Google Picker API getOAuthToken() throws server error after being published as private add-on

665 Views Asked by At

I have a script that makes uses of the Google Picker API. As I was testing it, it was running perfectly until I published it as a private add-on. Since then, the script getOAuthToken fails with the following (extremely unhelpful) error:

Exception: We're sorry, a server error occurred. Please wait a bit and try again. at getOAuthToken(Code:37:12)

What I have tried:

  1. creating a new API key
  2. adding the script owner to the GCP project (it's a generic Google account as per the company set up)
  3. enabling the Picker API in the add-on's GCP project instead of the old one and generating the a new key

The API key has the following settings:

  • application restrictions: HTTP referrers
  • website restrictions:
    • *.google.com
    • *.googleusercontent.com
  • API restrictions: Don't restrict key

These settings used to work before publication.

Also the code of the Google Picker is below. It's based on the boiler plate in the Google API documentation and used to work as is:

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
  <script>

    const pickFileType = '<?= fileType ?>';

    // IMPORTANT: Replace the value for DEVELOPER_KEY with the API key obtained
    // from the Google Developers Console.
    var DEVELOPER_KEY = 'intentionally removed';
    var DIALOG_DIMENSIONS = {width: 900, height: 500};
    var pickerApiLoaded = false;

    /**
     * Loads the Google Picker API.
     */
    function onApiLoad() {
      gapi.load('picker', {'callback': function() {
        pickerApiLoaded = true;
      }});
     }

    /**
     * Gets the user's OAuth 2.0 access token from the server-side script so that
     * it can be passed to Picker. This technique keeps Picker from needing to
     * show its own authorization dialog, but is only possible if the OAuth scope
     * that Picker needs is available in Apps Script. Otherwise, your Picker code
     * will need to declare its own OAuth scopes.
     */
    function getOAuthToken() {
      google.script.run.withSuccessHandler(createPicker)
          .withFailureHandler(showError).getOAuthToken();
    }

    /**
     * Creates a Picker that can access the user's spreadsheets. This function
     * uses advanced options to hide the Picker's left navigation panel and
     * default title bar.
     *
     * @param {string} token An OAuth 2.0 access token that lets Picker access the
     *     file type specified in the addView call.
     */
    function createPicker(token) {
      if (pickerApiLoaded && token) {
        const docsUploadView = new google.picker.DocsUploadView();
        docsUploadView.setIncludeFolders(true);
        
        const viewId = pickFileType === 'folder' ?
          google.picker.ViewId.FOLDERS : google.picker.ViewId.DOCUMENTS;
        
        const drivesView = new google.picker.DocsView(viewId);
        drivesView.setEnableDrives(true);
        drivesView.setIncludeFolders(true);
        if (pickFileType === 'folder') drivesView.setSelectFolderEnabled(true);
        
        const driveView = new google.picker.DocsView(viewId);
        driveView.setSelectFolderEnabled(true);
        driveView.setParent('root');
        if (pickFileType === 'folder') driveView.setIncludeFolders(true);

        console.log(`viewId = ${viewId}`);

        // const docsViewId = new google.picker.ViewGroup(google.picker.viewId.DOCS)
        //   .addView(viewId);

        var picker = new google.picker.PickerBuilder()
            // Instruct Picker to display only spreadsheets in Drive. For other
            // .addViewGroup(docsViewId)
            .addView(driveView)
            .addView(drivesView)
            // .addView(viewId)
            // .addView(docsUploadView)
            // Hide the navigation panel so that Picker fills more of the dialog.
            // .enableFeature(google.picker.Feature.NAV_HIDDEN)
            // .enableFeature(google.picker.Feature.MULTISELECT_ENABLED)
            .enableFeature(google.picker.Feature.SUPPORT_DRIVES)
            // Hide the title bar since an Apps Script dialog already has a title.
            .hideTitleBar()
            .setOAuthToken(token)
            .setDeveloperKey(DEVELOPER_KEY)
            .setCallback(pickerCallback)
            .setOrigin(google.script.host.origin)
            // Instruct Picker to fill the dialog, minus 2 pixels for the border.
            .setSize(DIALOG_DIMENSIONS.width - 2,
                DIALOG_DIMENSIONS.height - 2)
            .build();
        picker.setVisible(true);
      } else {
        showError('Unable to load the file picker.');
      }
    }

    /**
     * A callback function that extracts the chosen document's metadata from the
     * response object. For details on the response object, see
     * https://developers.google.com/picker/docs/result
     *
     * @param {object} data The response object.
     */
    function pickerCallback(data) {
      let selectedId;
      console.log(data);
      var action = data[google.picker.Response.ACTION];
      if (action == google.picker.Action.PICKED) {
        // const array = [['Nom', 'ID', 'URL']];
        const docs = data[google.picker.Response.DOCUMENTS];
        docs.forEach(doc => {
          var id = doc[google.picker.Document.ID];
          selectedId = id;
          // var url = doc[google.picker.Document.URL];
          // var title = doc[google.picker.Document.NAME];
          // array.push([title, id, url]);
        });

        google.script.run.withSuccessHandler(() => {
          google.script.run.showFront(true);
        }).writeVar(pickFileType, selectedId);
        
      } else if (action == google.picker.Action.CANCEL) {
        google.script.run.showFront(true);
      }
    }

    /**
     * Displays an error message within the #result element.
     *
     * @param {string} message The error message to display.
     */
    function showError(message) {
      document.getElementById('result').innerHTML = 'Error: ' + message;
    }
  </script>
</head>
<body>
  <div>
    <p id='result'></p>
  </div>
  <script src="https://apis.google.com/js/api.js?onload=onApiLoad"></script>
  <script>
    window.onload = getOAuthToken;
  </script>
</body>
</html>

EDIT: This is what the server-side getOAuthToken looks like. I'm leaving the comments inside.

/**
 * Gets the user's OAuth 2.0 access token so that it can be passed to Picker.
 * This technique keeps Picker from needing to show its own authorization
 * dialog, but is only possible if the OAuth scope that Picker needs is
 * available in Apps Script. In this case, the function includes an unused call
 * to a DriveApp method to ensure that Apps Script requests access to all files
 * in the user's Drive.
 *
 * @return {string} The user's OAuth 2.0 access token.
 */
function getOAuthToken() {
  DriveApp.getRootFolder();
  return ScriptApp.getOAuthToken();
}

EDIT 2

I think I'm getting closer to understanding. The script is now throwing these kinds of errors every time I try to access the Drive app. I get the same error when I do DriveApp.getFolderById(id) and

Unexpected error while getting the method or property getFileById on object DriveApp when I do DriveApp.getFileById(id).

I've added scopes to my manifest, but it's still not helping. This is the manifest:

{
  "timeZone": "Europe/Paris",
  "dependencies": {
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
    "https://www.googleapis.com/auth/drive",
    "https://www.googleapis.com/auth/spreadsheets",
    "https://www.googleapis.com/auth/script.container.ui",
    "https://www.googleapis.com/auth/userinfo.email",
    "https://www.googleapis.com/auth/spreadsheets"
    ]
}
1

There are 1 best solutions below

0
On BEST ANSWER

When using a default Cloud Platform project for your Apps Script, whatever APIs are used by the script are automatically enabled when the script project is saved.

That's not the case when you switch to a standard GCP project. In this case, the APIs are not automatically enabled, and you have to manually enable them on the GCP project:

Often an Apps Script application needs access to another Google API. This requires you to enable the API in the corresponding GCP project.

According to the documentation, this would only apply to Advanced Services, but it also applies to at least some standard services. See this issue:

Specifically this comment:

There are mentions about enabling APIs for advanced services here. But not for the standard services, I notified this to the documentation team.

Reference: