How can I debug a BadRequest error when importing a calendar event in an Apps Script?
Asked Answered
M

1

5

I've used the "Populate a team vacation calendar" sample at https://developers.google.com/apps-script/samples/automations/vacation-calendar to build an Apps Script to pull all Out Of Office events into a shared calendar. The script has been working fine for a few months. Without any changes to the script, now when calling Calendar.Events.import(event, TEAM_CALENDAR_ID); I get an error:

GoogleJsonResponseException: API call to calendar.events.import failed with error: Bad Request

I've run the script in the debugger, and the error doesn't provide any insight into what the error actually is, beyond that it's a 400 Bad Request. There are lots of other Q&A relating to date/time formatting, but the event variable I'm importing is from an earlier call to Calendar.Events.list(...) so it's already a JS event object produced by the api itself.

Here's a minimal reproducible example:

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());
  }
}

How do I debug this?

screenshot of error in debugger

Here's the entire script:

// 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');
}
Maurice answered 30/11, 2023 at 17:27 Comment(13)
Please provide a minimal reproducible example so that we can see the code that you are using to create the problem. Please post it into you question.Anthropocentric
Please post you code in the question. Personally, I do not follow links to offsite resourcesAnthropocentric
Edited question to include MRE.Maurice
I don't really know a lot about this but did you consider using this:function formatDateAsRFC3339(date) { return Utilities.formatDate(date, 'UTC', 'yyyy-MM-dd\'T\'HH:mm:ssZ'); } which was found at your link aboveAnthropocentric
Consider using formatDateAsRFC3339 where? I'm already using it to specify the date range in my call to Calendar.Events.list(...)Maurice
The error is likely from the parameters used in your Calendar.Events.import call. To troubleshoot, refer to the official documentation for Events: import. The document specifies the sequence as Calendar.Events.import(CALENDAR_ID, EVENT_OBJECT), while your current implementation uses Calendar.Events.import(EVENT_OBJECT, CALENDAR_ID). If the error persists, ensure that the calendar ID and the event object aren't corrupted by using console.log(). This can help verify the structure and contents for potential issues.Rafaelof
Thanks for the suggestion @Rafaelof In the doc you linked, all the examples (Java, Python, PHP, Ruby) do indeed have the calendar ID first. That said, I'm using Javascript, and the Apps Script editor's intellisense shows that resource (the event) comes first, and calendarId comes second (the full text is import(resource: Calendar_v3.Calendar.V3.Schema.Event, calendarId: string): Calendar_v3.Calendar.V3.Schema.Event). Also, this code has been working fine for at least a year. That said, I flipped the arguments and the same error occurred.Maurice
Also, I've inspected the arguments prior to calling the function, and while I'm not sure what constitutes as 'corruption', they look as expected. The event is one of several returned from Calendar.Events.list(..) and the calendarId is correct.Maurice
Does the minimal script posted above the "entire script" reproduce the same error?Seiler
Yes, both minimal script and entire script posted in the question produce the same error.Maurice
There's an ongoing report of the exact same behavior on the Google Issue Tracker. You might consider commenting on it, starring it, and waiting for new updates.Rafaelof
@Rafaelof I can confirm that the removal of outOfOfficeProperties (via delete event.outOfOfficeProperties) does fix the problem. Thanks for linking me to the bug report.Maurice
@Rafaelof Consider adding an answer.Seiler
R
6

A Google issue has been reported concerning the failure of Calendar.Events.import() with a 'Bad request' error since November 29, 2023.

According to the latest update,

outOfOfficeProperties (via delete event.outOfOfficeProperties) does fix the problem.


Reference


Rafaelof answered 1/12, 2023 at 17:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.