google-apps-scriptgoogle-calendar-apihttp-status-code-400bad-request

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


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

Solution

  • 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