- Overview
- Description
- Deploy Web Apps
- Redeploying Web Apps without Changing URL of Web Apps for new IDE
- 5 situations for Web Apps
- Understanding Flow of Request to Web Apps Created by Google Apps Script Added at July 25, 2023
- How to use dev mode from outside
- Event object of Web Apps
- Logs in Web Apps for Google Apps Script
- Limitation of simultaneous connection to Web Apps
- Error messages
- CORS in Web Apps
- Confidentiality of scripts for Web Apps
- Sample script of server side
- Sample scripts of client side
- Sample scripts of client side by various languages
- Status code from Web Apps
- Concurrent access to Web Apps
- Implementing Pseudo 2FA for Web Apps
- Request Web Apps using Fetch API of Javascript with access token
- Executing Google Apps Script with Service Account Added at October 19, 2023
- Workarounds
- Applications
- Sample situations
- References
This is a report to take advantage of Web Apps with Google Apps Script (GAS).
There is "Web Apps" as one of the applications using Google Apps Script (GAS). I sometimes use this Web app. But I have only a little information for the specification of Web Apps. So in order to take more advantage of Web Apps, I investigated and summarized this. The aim of this report is to become one of the basic information for creating various applications using Web Apps with GAS.
When Web Apps is deployed, you can see the following window.
New IDE for Google Apps Script has finally been released at December 7, 2020. Ref For new IDE, please open a dialog by "Deploy" -> "New deployment". And, please select "Web app" at "Select type. By this, you can see the following dialog.
On the other hand, when you use old IDE, you can see the following dialog.
For setting, generally, the following flow is used.
-
On script editor
-
Publish -> Deploy as web app...
- Project version:
- "New" and input the description.
- Pattern 1: Execute the app as: (old IDE), Execute as: (new IDE)
- Me (old IDE and new IDE)
- Who has access to the app:
- Only myself (old IDE and new IDE)
- Anyone (old IDE), Anyone with Google account (new IDE)
- Anyone, even anonymous (old IDE), Anyone (new IDE)
- Who has access to the app:
- Me (old IDE and new IDE)
- Pattern 2: Execute the app as: (old IDE), Execute as: (new IDE)
- User accessing the web app (old IDE and new IDE)
- Who has access to the app: (old IDE), Who has access (new IDE)
- Only myself (old IDE and new IDE)
- Anyone (old IDE), Anyone with Google account (new IDE)
- Who has access to the app: (old IDE), Who has access (new IDE)
- User accessing the web app (old IDE and new IDE)
- Click Deploy.
- Project version:
-
There are Me and User accessing the web app for "Execute the app as:" and Only myself, Anyone and Anyone, even anonymous for "Who has access to the app:" as the options. When Me and User accessing the web app are selected for "Execute the app as:", each "Who has access to the app:" has 3 and 2 options, respectively. From above flow, It is found that there are 5 situations for deploying Web Apps. For each situation, there are 2 methods of GET and POST.
-
At new IDE,
-
Execute the app as: became Execute as. And the values are Me and User accessing the web app. These are the same with the old IDE.
-
Who has access to the app: became Who has access. About the values, please check the following list.
- When Execute as is Me, the values of Who has access are Only myself, Anyone with Google account and Anyone. In this case, the values of Only myself, Anyone with Google account and Anyone of new IDE are the same with Only myself, Anyone and Anyone, even anonymous, respectively.
- When Execute as is User accessing the web app, the values of Who has access are Only myself and Anyone with Google account. These are the same with Only myself and Anyone of the old IDE, respectively.
-
In this report, I would like to introduce about the specification for Web Apps deployed by 5 situations.
At March 15, 2021, one endpoint is created for one deployment. Ref By this, when you redeploy "Web Apps", the endpoint is changed. Because the deployment ID is changed. It seems that this it the new specification. In this report, I would like to introduce the method for redeploying Web Apps without changing the URL of Web Apps for new IDE.
The detail document can be seen at https://gist.github.com/tanaikech/ebf92d8f427d02d53989d6c3464a9c43.
- Situation 1
- "Execute the app as:" : Me
- "Who has access to the app:": Only myself
- Situation 2
- "Execute the app as:" : Me
- "Who has access to the app:": Anyone
- Situation 3
- "Execute the app as:" : Me
- "Who has access to the app:": Anyone, even anonymous
- Situation 4
- "Execute the app as:" : User accessing the web app
- "Who has access to the app:": Only myself
- Situation 5
- "Execute the app as:" : User accessing the web app
- "Who has access to the app:": Anyone
There are 5 situations for the deployed Web Apps, as mentioned above. And the methods for accessing to the deployed Web Apps are divided into 2 types. Those are the method which accesses using browser and the method which accesses using Curl, Google Apps Script and so on which don't use browser. Each method is used by the owner of Web Apps and the client users. The flow which summarized them is as follows.
- For Situation 1, 4
- Owner can access and run the script of Web Apps by the login to Google.
- For Situation 2, 5
- Owner can access and run the script of Web Apps by the login to Google.
- For Situation 3
- Owner can access and run the script of Web Apps without the login to Google.
- For Situation 1, 4
- Client users cannot access and run the script of Web Apps.
- For Situation 2, 5
- Client users can access and run the script of Web Apps by the login to Google.
- For Situation 3
- Client users can access and run the script of Web Apps without the login to Google.
- For Situation 1, 4
- Owner can access and run the script of Web Apps by using the access token.
- For Situation 2, 5
- Owner can access and run the script of Web Apps by using the access token.
- For Situation 3
- If the script of Web Apps uses some scopes, owner has to authorize the scopes by own browser.
- Owner can access and run the script of Web Apps without the login to Google.
- For Situation 1, 4
- Client users cannot access and run the script of Web Apps.
- For Situation 2, 5
- At first, the project that Web Apps was deployed has to be shared with client users who use Web Apps.
- I confirmed that from April 11, 2018, it is required to be shared the project to access to Web Apps. This might be due to the update of Google.
- If the script of Web Apps uses some scopes, client users have to authorize the scopes by own browser.
- Client users can access and run the script of Web Apps without the login to Google.
- At first, the project that Web Apps was deployed has to be shared with client users who use Web Apps.
- For Situation 3
- If the script of Web Apps uses some scopes, client users have to authorize the scopes by own browser.
- Client users can access and run the script of Web Apps without the login to Google.
The simple explanations for above 5 situations were summarized as the following table. Owner and client users mean the owner who deployed Web Apps and the users who use the deployed Web Apps, respectively.
Situations | Script of Web Apps | Authorization for scopes | Owner | Client users | Share project |
---|---|---|---|---|---|
Situation 1 | Run as owner. | Only owner | Access with access token. | Cannot access. | No |
Situation 2 | Run as owner. | Only owner | Access with access token. | Access with access token. | Yes |
Situation 3 | Run as owner. | Only owner | Access without access token. | Access without access token. | No |
Situation 4 | Run as each user. | Each user | Access with access token. | Cannot access. | No |
Situation 5 | Run as each user. | Each user | Access with access token. | Access with access token. | Yes |
For example, at situation 5, the script of Web Apps is run as each user (owner and client users). The authorization for the scopes of scripts of Web Apps is required to run the scripts. This authorization has to be done by each user (owner and client users) using own browser. The access token is required for each user (owner and client users) to access to Web Apps. In order to access to Web Apps from client users, the project deployed Web Apps has to be shared with the users.
-
When Web Apps is deployed as
"Execute the app as:" : Me
by owner, the authorization screen is automatically opened. When the owner authorizes it, owner and client users can run the scripts of Web Apps as the owner.-
For example, in this situation, when
Session.getEffectiveUser().getEmail()
is run in this situation by the client users, the retrieved email is the owner's email. From this, it is found that the script is run as the owner. -
For example, in this situation, when
DriveApp.createFile(blob)
is run at the script of Web Apps, the file is created in the owner's Google Drive.
-
-
When Web Apps is deployed as
"Execute the app as:" : User accessing the web app
by owner, the authorization screen is NOT opened automatically. So before owner and client users access to Web Apps, they have to manually authorize to use the scopes by own browser.-
If some scopes are used for the scripts of Web Apps, owner and client users have to authorize them only one time using own browser, before it accesses to Web Apps.
-
The URL for authorizing is
https://script.google.com/macros/s/#####/exec
which is the same to URL of Web Apps. When owner and client users access to the URL using own browser, the following authorization screen is displayed. -
When you see above screen, please click "REVIEW PERMISSIONS". And select account and authorize to use the scopes. After owner and client users authorized the scopes, they can run the script of Web Apps.
-
-
For example, in this situation, when
Session.getEffectiveUser().getEmail()
is run in this situation by the client users, the retrieved email is the each user's email. From this, it is found that the script is run as each user. -
For example, in this situation, when
DriveApp.createFile(blob)
is run at the script of Web Apps, the file is created in the each user's Google Drive.
-
-
When Web Apps is deployed as
"Who has access to the app:": Only myself
or"Who has access to the app:": Anyone
by owner, the owner and client users have to access and run the script of Web Apps with own access token.-
At least, one of scopes for using Drive API is required to be included in the access token. For example, those are
https://www.googleapis.com/auth/drive.readonly
https://www.googleapis.com/auth/drive.files
https://www.googleapis.com/auth/drive
-
Even if the server script uses the scopes except for scripts to use Drive API, the scopes except for scopes for Drive API are not required to be included. Because the use of other scopes is authorized by the browser, before it accesses to Web Apps. This means that when it accesses to Web Apps by GET or POST request, only the scopes for using Drive API are required.
-
For example, when you want to access to Web Apps using GAS, even if you use
{"Authorization": "Bearer " + ScriptApp.getOAuthToken()}
to the headers, when the error of<TITLE>Unauthorized</TITLE>
occurs, please confirm the scopes of the scripts at the script editor. Ifhttps://www.googleapis.com/auth/drive
is not included in the scopes, please add it. In order to add it, for example, you can put// DriveApp.getFiles()
in the scripts as a comment. By this, the script editor will automatically includehttps://www.googleapis.com/auth/drive
.
-
-
If no scopes are used for the scripts of Web Apps, owner and client users can run the script of Web Apps without the authorization of scopes using own browser. But the access token is required to access to Web Apps, if it is
"Who has access to the app:": Only myself
or"Who has access to the app:": Anyone
. -
Only when it is
"Who has access to the app:": Anyone, even anonymous
, owner and client users can access to Web Apps without the access token.
When client users access to Web Apps with Situation 2 and 5, the owner of Web Apps is required to share the project deployed Web Apps with the client users. If the project is not shared, the error message of <title>Google Drive - Access Denied</title>
is returned. For example, if the project is the container-bound script with Spreadsheet, please share the spreadsheet with the client users.
For sharing the project, you can manually share it and use "VIEW" to the permissions.
Also you can share the project using GAS scripts. The sample GAS script for sharing the project with the client users is as follows. When you use this, please input email and fileId. For example, if the project is the container-bound script with Spreadsheet, the fileId is that of spreadsheet. If you want to send a notification email when the project is shared, please modify from sendNotificationEmail=false
to sendNotificationEmail=true
.
// DriveApp.getFiles() // This comment is used for including a scope of https://www.googleapis.com/auth/drive. Of course, you can use Manifests for the project.
var email = "### Client user's email address ###";
var fileId = "### fileId of project ###";
var url =
"https://www.googleapis.com/drive/v3/files/" +
fileId +
"/permissions?sendNotificationEmail=false";
var params = {
method: "post",
headers: { Authorization: "Bearer " + ScriptApp.getOAuthToken() },
contentType: "application/json",
payload: JSON.stringify({
role: "reader",
type: "user",
emailAddress: email,
}),
muteHttpExceptions: true,
};
var res = UrlFetchApp.fetch(url, params);
Logger.log(res);
Here, I would like to introduce a report for understanding the flow of the request to Web Apps created by Google Apps Script. There might be a case that various applications using the Web Apps are created and the Web Apps are used as the webhook. In that case, it is considered that when you have understood the flow of requests to the Web Apps, your goal might be able to be smoothly achieved. In this report, I would like to introduce the information about it.
You can see the detail of this at the following my gist.
https://gist.github.com/tanaikech/131ba814a1f6012fd6a5ffe11789971f
When you deploy Web Apps, you can see the link labeled latest code. The link is like https://script.google.com/macros/s/###/dev
. The official document of Google is as follows.
This URL can only be accessed by users who have edit access to the script. This instance of the app always runs the most recently saved code — not necessarily a formal version — and is intended for quick testing during development.
When you access to the link of latest code using your browser under you login to Google, you can access to Web Apps with the dev mode. But if you want to access to Web Apps with the dev mode from outside, there are no documents for the method. Here, it introduces such method.
When it accesses to the URL of latest code from outside, the login screen is returned. This situation indicates that it is required to use the access token for accessing to the URL. So as a sample, I show you the sample curl command as follows. The requirement scope is https://www.googleapis.com/auth/drive
.
curl -L \
-H "Authorization: Bearer ### access token ###" \
"https://script.google.com/macros/s/#####/dev"
Also from Standard Query Parameters, you can use the following method using the access token as the query parameter.
curl -L "https://script.google.com/macros/s/#####/dev?access_token=### access token ###"
- Replace
### access token ###
to the value retrieved by ScriptApp.getOAuthToken(). - Replace
https://script.google.com/macros/s/#####/dev
to your endpoint retrieved by deploying Web Apps.
curl -L \
-H "Authorization: Bearer ### access token ###" \
-d "key=value" \
"https://script.google.com/macros/s/#####/dev"
Also from Standard Query Parameters, you can use the following method using the access token as the query parameter.
curl -L \
-d "key=value" \
"https://script.google.com/macros/s/#####/dev?access_token=### access token ###"
- Replace
### access token ###
to the value retrieved by ScriptApp.getOAuthToken(). - Replace
https://script.google.com/macros/s/#####/dev
to your endpoint retrieved by deploying Web Apps. - I used
-d "key=value"
for the post method. If you don't want to put the values, please replace to-d ""
.
- By using above method, you can use the latest script of Web Apps from outside.
- If you want to use the dev mode as simple test, there is a simple method for retrieving the access token.
- Login to Google and open the script editor.
- Copy and paste the following script.
function getAccesstoken() {Logger.log(ScriptApp.getOAuthToken())} // DriveApp.getFiles()
- Run the function
getAccesstoken()
and retrieve the access token.
By this, you can test it using the retrieved access token.
-
This method was answered at https://stackoverflow.com/a/54191688/7108653.
-
The reason that
-d
is used instead of-X POST
can be seen at this thread.
When Web Apps is used, doGet(e)
and doPost(e)
are used for the GET and POST method, respectively. Here, it introduces the event object of e
.
The sample script for GET and POST method of Web Apps is as follows.
function doGet(e) {
e.method = "GET";
return ContentService.createTextOutput(JSON.stringify(e)).setMimeType(
ContentService.MimeType.JSON
);
}
function doPost(e) {
e.method = "POST";
return ContentService.createTextOutput(JSON.stringify(e)).setMimeType(
ContentService.MimeType.JSON
);
}
As a sample client, curl is used.
$ curl -L "https://script.google.com/macros/s/#####/exec?key1=value1&key2=value2&key3=value3"
{
"parameter": {
"key1": "value1",
"key2": "value2",
"key3": "value3"
},
"contextPath": "",
"contentLength": -1,
"queryString": "key1=value1&key2=value2&key3=value3",
"parameters": {
"key1": ["value1"],
"key2": ["value2"],
"key3": ["value3"]
},
"method": "GET"
}
$ curl -L -d "key1=value1" -d "key2=value2" "https://script.google.com/macros/s/#####/exec?key3=value3"
{
"parameter": {
"key1": "value1",
"key2": "value2",
"key3": "value3"
},
"contextPath": "",
"contentLength": 23,
"queryString": "key3=value3",
"parameters": {
"key1": ["value1"],
"key2": ["value2"],
"key3": ["value3"]
},
"postData": {
"type": "application/x-www-form-urlencoded",
"length": 23,
"contents": "key1=value1&key2=value2",
"name": "postData"
},
"method": "POST"
}
$ curl -L -d '{"key1": "value1", "key2": "value2"}' "https://script.google.com/macros/s/#####/exec?key3=value3"
{
"parameter": {
"key3": "value3",
"{\"key1\": \"value1\", \"key2\": \"value2\"}": ""
},
"contextPath": "",
"contentLength": 36,
"queryString": "key3=value3",
"parameters": {
"key3": ["value3"],
"{\"key1\": \"value1\", \"key2\": \"value2\"}": [""]
},
"postData": {
"type": "application/x-www-form-urlencoded",
"length": 36,
"contents": "{\"key1\": \"value1\", \"key2\": \"value2\"}",
"name": "postData"
},
"method": "POST"
}
$ curl -L -H "Content-Type: application/json" -d '{"key1": "value1", "key2": "value2"}' "https://script.google.com/macros/s/#####/exec?key3=value3"
Although the JSON object is sent as application/json
and Web Apps recognizes the data as application/json
, the JSON parse of contents
is not parsed. Please be careful this. In this case, it is required to parse using JSON.parse()
.
{
"parameter": {
"key3": "value3"
},
"contextPath": "",
"contentLength": 36,
"queryString": "key3=value3",
"parameters": {
"key3": ["value3"]
},
"postData": {
"type": "application/json",
"length": 36,
"contents": "{\"key1\": \"value1\", \"key2\": \"value2\"}",
"name": "postData"
},
"method": "POST"
}
In this pattern, the filename is added to the URL as follows.
https://script.google.com/macros/s/#####/exec/fileName.txt
In this case, the authorization is required. Please be careful this. So when the curl command is used, the access token is required to be used as follows.
$ curl -L \
-H "Authorization: Bearer ###" \
-H "Content-Type: application/json" \
-d '{"key1": "value1", "key2": "value2"}' \
"https://script.google.com/macros/s/#####/exec/fileName.txt?key3=value3""
Also, when you have already logged in Google account, you can directly access to the URL. But in this case, it is the GET method.
When you run above curl command, you can retrieve the following values. fileName.txt
can be retrieved by pathInfo
.
{
"pathInfo": "fileName.txt",
"contextPath": "",
"postData": {
"contents": "{\"key1\": \"value1\", \"key2\": \"value2\"}",
"length": 36,
"name": "postData",
"type": "application/json"
},
"contentLength": 36,
"parameter": {
"key3": "value3"
},
"parameters": {
"key3": ["value3"]
},
"queryString": "key3=value3",
"method": "POST"
}
In the current stage, it seems that pathInfo
can be used with the access token. It supposes that the following sample script is used.
function doGet(e) {
return ContentService.createTextOutput(JSON.stringify(e));
}
When you log in to your Google account and you access https://script.google.com/macros/s/###/exec/sample.txt
with your browser, {"contextPath":"","parameter":{},"pathInfo":"sample.txt","contentLength":-1,"parameters":{},"queryString":""}
can be seen.
In this case, when you access it without logging in Google account, even when Web Apps is deployed as Execute as: Me
and Who has access to the app: Anyone
, the log in screen is opened. Please be careful about this.
And, if you want to access with https://script.google.com/macros/s/###/exec/sample.txt
using a script, please request it by including the access token. The sample curl command is as follows. In this case, the access token can be used as the query parameter. Please include one of the scopes of Drive API in the access token.
curl -L "https://script.google.com/macros/s/###/exec/sample.txt?access_token=###"
By this, the following result is returned.
{"contextPath":"","queryString":"access_token=###"},"pathInfo":"sample.txt","parameters":{"access_token":["###"]},"contentLength":-1}
This is a report for retrieving the logs in Web Apps for Google Apps Script, when it requests to the Web Apps.
const doGet = (e) => {
Logger.log(`GET method: ${JSON.stringify(e)}`);
console.log(`GET method: ${JSON.stringify(e)}`);
return ContentService.createTextOutput(
JSON.stringify({ method: "GET", e: e })
);
};
const doPost = (e) => {
Logger.log(`POST method: ${JSON.stringify(e)}`);
console.log(`POST method: ${JSON.stringify(e)}`);
return ContentService.createTextOutput(
JSON.stringify({ method: "POST", e: e })
);
};
- This Web Apps is deployed as
Execute the app as: Me
andWho has access to the app: Anyone, even anonymous
.
-
Google Apps Script of standalone type WITHOUT linking Google Cloud Platform (GCP) Project
- In this case, you can retrieve this standalone Google Apps Script by directly creating.
-
Google Apps Script of standalone type WITH linking Google Cloud Platform (GCP) Project
- In this case, you can retrieve this standalone Google Apps Script by this flow.
To above Web Apps of doGet
and doPost
, it requests with the following 4 patterns.
-
For
doGet
.$ curl -L "https://script.google.com/macros/s/###/exec"
-
For
doPost
.$ curl -L -d "key=value" "https://script.google.com/macros/s/###/exec"
-
For
doGet
. Access token is used.$ curl -L -H "Authorization: Bearer ###" "https://script.google.com/macros/s/###/exec"
-
For
doPost
. Access token is used.$ curl -L -H "Authorization: Bearer ###" -d "key=value" "https://script.google.com/macros/s/###/exec"
The conditions which can confirm the logs are as follows.
Without access token | With access token | |
---|---|---|
Without linking GCP | Apps Script Dashboard | |
With linking GCP | Stackdriver | Apps Script Dashboard and Stackdriver |
From above results, it was found as follows.
-
If you use the default Google Apps Script project without linking GCP, in order to retrieve the logs which requested to the Web Apps, please access to the Web Apps using the access token, even when the Web Apps is deployed as
Execute the app as: Me
andWho has access to the app: Anyone, even anonymous
. -
If you use the Google Apps Script project with linking GCP, you can retrieve all logs of users who accessed to the Web Apps at Stackdriver, even when the Web Apps is deployed as
Execute the app as: Me
andWho has access to the app: Anyone, even anonymous
. -
In this case, the logs couldn't be seen with
Logger.log
for above all situations.
In the current stage, when the access token is used for XMLHttpRequest and fetch of Javascript in the request headers, the error related to CORS occurs. So, in this report, I would like to propose the workaround for resolving this issue.
The limitation of simultaneous connection is also investigated. The fetchAll method added by recent Google's update was used for this investigation. I have reported that the fetchAll method is worked by the asynchronous processing. This can be used for measuring the limitation of simultaneous connection. From the result of measurement, it was found that the limitation of simultaneous connection to one Web Apps server is under 30. This is the same with scripts.run method of Apps Script API.
When "Current limitations" of "Quotas for Google Services", it says that the simultaneous executions is 30.
When the error messages are returned from Web Apps, you can see the messages into the tag of <title>### Error message ###</title>
including in HTML output. From the error messages, you can know the reason of the error using the following table. I think that there may be other errors. So if you found them, when you tell me them, I'm glad.
Execute the app as | Who has access to the app | Access | Status code | Error messages | Reason |
---|---|---|---|---|---|
User accessing the web app | Only myself, Anyone |
Owner, Users |
200 | Authorization needed | Scopes for scripts of Web Apps are not authorized. |
User accessing the web app, Me |
Only myself, Anyone |
Owner, Users |
200 | Meet Google Drive – One place for all your files | No access token. |
For all settings | For all settings | Owner, Users |
200 | Error | "Service invoked too many times in a short time: exec qps. Try Utilities.sleep(1000) between calls." is shown in Body. |
User accessing the web app, Me |
Only myself, Anyone |
Owner, Users |
401 | Unauthorized | Bad access token. No required scopes. |
User accessing the web app, Me |
Anyone | Users | 403 | Google Drive - Access Denied | Project of Web Apps is not shared with users. |
User accessing the web app, Me |
Only myself | Users | 404 | Google Drive -- Page Not Found | Users cannot access. |
In this section, I would like to introduce CORS in Web Apps. As a sample situation, it supposes that Web Apps is requested using GET and POST method with Javascript. The settings of Web Apps are Execute the app as: Me
and Who has access to the app: Anyone, even anonymous
.
The Javascript of the client side, which was used for testing, is as follows.
const url = "https://script.google.com/macros/s/###/exec";
function get() {
fetch(url)
.then((res) => {
console.log(res.status);
return res.text();
})
.then((res) => console.log(res));
}
function post() {
const obj = { key: "value" };
fetch(url, { method: "POST", body: JSON.stringify(obj) })
.then((res) => {
console.log(res.status);
return res.text();
})
.then((res) => console.log(res));
}
const doGet = (e) => {};
const doPost = (e) => {};
For both requests, the following response is returned. Error related to CORS occurs.
Access to fetch at 'https://script.google.com/macros/s/###/exec' from origin 'https://###script.googleusercontent.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
const doGet = (e) => HtmlService.createHtmlOutput();
const doPost = (e) => HtmlService.createHtmlOutput();
For both requests, the following response is returned. Error related to CORS occurs.
Access to fetch at 'https://script.google.com/macros/s/###/exec' from origin 'https://###script.googleusercontent.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
const doGet = (e) => ContentService.createTextOutput();
const doPost = (e) => ContentService.createTextOutput();
For both requests, the following response is returned. No error occurs.
200
const doGet = (e) => ContentService.createTextOutput();
const doPost = (e) => ContentService.createTextOutput();
In this case, axios is used. This sample referred Stackoverflow.
const url = "https://script.google.com/macros/s/###/exec";
async function get() {
const res = await axios.get(url);
console.log(res);
}
async function post() {
const obj = { key: "value" };
const res = await axios.post(url, obj);
console.log(res);
}
In this case, at post()
, the object is not converted to the string with JSON.stringify
.
- No error occurs at
get()
. - Error related to CORS occurs at
post()
.
Please convert the object to the string with JSON.stringify
. By this, the error can be removed. No error occurs.
async function post() {
const obj = { key: "value" };
const res = await axios.post(url, JSON.stringify(obj));
console.log(res);
}
When above script didn't work, please set "Content-Type": "text/plain"
to the header and test it again. On the other hand, when the following script is used, No error occurs at post()
.
function post() {
const obj = { key: "value" };
fetch(url, { method: "POST", body: obj })
.then((res) => {
console.log(res.status);
return res.text();
})
.then((res) => console.log(res));
}
As the result, in order to prevent the error related to CORS, it was found the following important points.
-
It is required to return
ContentService.createTextOutput()
andContentService.createTextOutput("done")
fromdoGet
anddoPost
. -
For POST method, the data is required to be converted to the string and sent to Web Apps.
-
If you don't want to publish the scripts of Web Apps, you can achieve it using Situation 3.
- In this case, anybody can access the Web Apps.
-
If you want to make only the special users access to Web Apps, you can achieve it using Situation 2 and 5.
- In this case, the scripts of Web Apps can be seen by the users, because the project of scripts has to be shared with the users.
The simple sample-script of server side (Web Apps) is as follows. When the client users access to this server, the server returns URL query and the request body which were sent by the client. And also a text file is created. In this script, the scope of https://www.googleapis.com/auth/drive
is used for creating a text file. So when you use this script at Situation 4 and Situation 5 as mentioned above, at first, please authorize the scope by accessing to Web Apps using your browser. After the authorization was done, the text file is created by requesting the following scripts for the client side.
function feedback(e, method) {
var id = "";
var user = Session.getEffectiveUser().getEmail();
if (e.parameter.key1 == "value1") {
id = DriveApp.createFile(
"sample.txt",
"Created by " + user,
MimeType.PLAIN_TEXT
).getId();
}
return ContentService.createTextOutput(
JSON.stringify({
result: Object.keys(e.parameters).map(function (f) {
return f + "," + e.parameters[f];
}),
method: method,
createdFile: id ? id + " was created by " + user : "Not created.",
})
).setMimeType(ContentService.MimeType.JSON);
}
function doGet() {
return feedback(e, "GET");
}
function doPost() {
return feedback(e, "POST");
}
curl -GL \
-H "Authorization: Bearer ### your access token ###" \
-d "key1=value1" \
-d "key2=value2" \
"https://script.google.com/macros/s/#####/exec?key3=value3"
// DriveApp.getFiles() // This comment is used for including a scope of https://www.googleapis.com/auth/drive.
var params = {
method: "GET",
headers: { Authorization: "Bearer " + ScriptApp.getOAuthToken() },
muteHttpExceptions: true,
};
var url =
"https://script.google.com/macros/s/#####/exec?key1=value1&key2=value2&key3=value3";
var res = UrlFetchApp.fetch(url, params);
Logger.log(res);
curl -L \
-H "Authorization: Bearer ### your access token ###" \
-d "key1=value1" \
-d "key2=value2" \
"https://script.google.com/macros/s/#####/exec?key3=value3"
// DriveApp.getFiles() // This comment is used for including a scope of https://www.googleapis.com/auth/drive.
var params = {
method: "POST",
headers: { Authorization: "Bearer " + ScriptApp.getOAuthToken() },
payload: { key1: "value1", key2: "value2" },
muteHttpExceptions: true,
};
var url = "https://script.google.com/macros/s/#####/exec?key3=value3";
var res = UrlFetchApp.fetch(url, params);
Logger.log(res);
curl -GL \
-d "key1=value1" \
-d "key2=value2" \
"https://script.google.com/macros/s/#####/exec?key3=value3"
var params = {
method: "GET",
muteHttpExceptions: true,
};
var url =
"https://script.google.com/macros/s/#####/exec?key1=value1&key2=value2&key3=value3";
var res = UrlFetchApp.fetch(url, params);
Logger.log(res);
curl -L \
-d "key1=value1" \
-d "key2=value2" \
"https://script.google.com/macros/s/#####/exec?key3=value3"
var params = {
method: "POST",
payload: { key1: "value1", key2: "value2" },
muteHttpExceptions: true,
};
var url = "https://script.google.com/macros/s/#####/exec?key3=value3";
var res = UrlFetchApp.fetch(url, params);
Logger.log(res);
Now, you can see the following 11 sample scripts at https://gist.github.com/tanaikech/a72aab0242012362c46ec69031c720d5.
- curl
- Google Apps Script
- Javascript
- ajax
- Node.js
- axios
- angular
- go
- python
- php
- powershell
Here, I would like to introduce the status code returned from Web Apps.
As the preparation, Web Apps was deployed with "Execute the app as" and "Who has access to the app" as Me
and Anyone, even anonymous
, respectively. The sample script for Web Apps is as follows.
function doGet(e) {
return ContentService.createTextOutput("GET: Done.");
}
At first, the status code was checked using Google Apps Script. The deployed Web Apps was requested with the following script.
function myFunction() {
var url_exec = "https://script.google.com/macros/s/###/exec";
var url_dev = "https://script.google.com/macros/s/###/dev";
var res = UrlFetchApp.fetchAll([{ url: url_exec }, { url: url_dev }]);
res.forEach(function (e) {
Logger.log(e.getResponseCode());
});
}
In this case, the status code of 200
was obtained for both endpoints of exec
and dev
.
In order to retrieve the status code with curl, curl -s -o /dev/null -w "%{http_code}" http://www.example.org/
was used. This is from this thread. Here, the status code was investigated using the curl command. Because the curl can access by 2 kinds of request by the options as showing below.
-
--include
: Include the HTTP response headers in the output. The HTTP response headers can include things like server name, cookies, date of the document, HTTP version and more... -
--head
: (HTTP FTP FILE) Fetch the headers only! HTTP-servers feature the command HEAD which this uses to get nothing but the header of a document. When used on an FTP or FILE file, curl displays the file size and last modification time only.
Using above options, the following 4 patterns were investigated.
- Request to the endpoint of
https://script.google.com/macros/s/###/exec
using the option--include
.curl -sL --include -o /dev/null -w "%{http_code}" "https://script.google.com/macros/s/###/exec"
200
was returned.
- Request to the endpoint of
https://script.google.com/macros/s/###/dev
using the option--include
.curl -sL --include -o /dev/null -w "%{http_code}" "https://script.google.com/macros/s/###/dev"
200
was returned.
- Request to the endpoint of
https://script.google.com/macros/s/###/exec
using the option--head
.curl -sL --head -o /dev/null -w "%{http_code}" "https://script.google.com/macros/s/###/exec"
403
was returned.
- Request to the endpoint of
https://script.google.com/macros/s/###/dev
using the option--head
.curl -sL --head -o /dev/null -w "%{http_code}" "https://script.google.com/macros/s/###/dev"
200
was returned.
As the result, it was found that when the Web Apps of the endpoint of https://script.google.com/macros/s/###/exec
was requested with the option --head
, the status code of 403
was obtained.
When the option --head
is used for the curl command, from the document, this means that it requests only header and doesn't request the body. By this, it was found that the status code of 403
was returned.
Here, why was the status code of 200
returned for both options of --include
and --head
when it requests to the endpoint of dev
? It is considered that the reason of this is due to that the login screen was returned. When the endpoint of dev
is accessed, it is required to use the access token. When the access token is not used, the login screen is returned. In this case, the status code of 200
is returned. As the test case, when the access token is used for the endpoint of dev
using below curl command,
curl -sL --head -H "Authorization: Bearer ###" -o /dev/null -w "%{http_code}" "https://script.google.com/macros/s/###/dev"
The status code of 403
was returned. From this result, the following results were obtained.
- When only the header is retrieved under that the Web Apps works fine,
403
is returned. - When the login screen is returned,
200
is returned.
As one of applications using this situation, there is the registration of webhook at Trello's REST API.
When the Web Apps which was deployed as Execute the app as: Me
and Who has access to the app: Anyone, even anonymous
is registered as the webhook, an error of {"message":"URL (https://script.google.com/macros/s/###/exec) did not return 200 status code, got 403","error":"ERROR"}
occurs. The reason of this error is due to above situation. So as the workaround for the registration, you can use the following flow.
- Before you run the script, please set the Web Apps as follows. - "Execute the app as" and "Who has access to the app" are
Me
andOnly myself
, respectively. - After the response of like
{"id":"###","description":"sample","idModel":"###","callbackURL":"https://script.google.com/macros/s/###/exec","active":true}
was retrieved, please set the Web Apps as follows. - "Execute the app as" and "Who has access to the app" areMe
andAnyone, even anonymous
, respectively.
By this flow, the webhook can be used.
This is a sample script of Google Apps Script for the registration of webhook URL.
var url = "https://api.trello.com/1/tokens/###/webhooks/?key=###";
var payload = {
callbackURL: "https://script.google.com/macros/s/###/exec",
idModel: "###",
description: "sample",
};
var options = { method: "post", payload: payload };
var res = UrlFetchApp.fetch(url, options);
Logger.log(res.getContentText());
-
- I answered to this thread in Stackoverflow. Ref
-
Benchmark: Concurrent Writing to Google Spreadsheet using Form
-
When the users try to write to a Spreadsheet using a form, the developers have to consider the concurrent submission from the form. For example, when multiple users submit the data with the form simultaneously, all data are possibly not to be saved to the Spreadsheet. So it is considered that it is important to know the information about the concurrent writing to Google Spreadsheet using a form. In this report, such a situation was investigated.
-
As the result, when the success rate for writing concurrently to Google Spreadsheet is investigated, it was found that the concurrent writing with Web Apps created by Google Apps Script was suitable rather than Google Form. The threshold number of users for succeeding to write all data to Spreadsheet was 60 for Web Apps and 35 for Google Form, respectively. And, when Web Apps is used, it was also found that Lock Service and the long wait time was definitely required to be used for the multiple submission.
-
In Google Apps Script, there is the Web Apps. When Web Apps is used, the users can execute Google Apps Script using HTML and Javascript. This can be applied to various applications. When the Web Apps is deployed with "Anyone", anyone can access the Web Apps. And, there is the case that Web Apps deployed with "Anyone" is required to be used. Under this condition, when 2 Factor Authentication (2FA) can be implemented, it is considered that the security can be higher and it leads to giving various directions for the applications using Web Apps. In this report, I would like to introduce the method for implementing the pseud 2FA for Web Apps deployed with “Anyone” using Google Apps Script.
You can see the detail of this report at https://gist.github.com/tanaikech/7a15164b1227e2ec2231fce24ae9daf2.
In the current stage, when you want to request Web Apps with the access token, when the access token is including the request header, an error related to CORS occurs. Please be careful about this.
So, in this case, please include the access token in the query parameter. By this, the request can be worked. The sample script is as follows.
const accessToken = "###"; // Please set your access token.
const url = "https://script.google.com/macros/s/###/exec?access_token=" + accessToken;
fetch(url)
.then((res) => res.text())
.then((res) => console.log(res));
By this modification, you can access Web Apps with the access token.
By the way, for example, when the curl command is used, the access token can be included in the request header as follows.
$ curl -L \
-H "Authorization: Bearer ###" \
"https://script.google.com/macros/s/###/exec"
And also, the following curl command can be used.
$ curl -L "https://script.google.com/macros/s/###/exec?access_token=###"
You can see the detailed report at the following gist and Medium.
- https://gist.github.com/tanaikech/304fea821ca36b8e9ccebe9814eaed82
- https://medium.com/@tanaike/executing-google-apps-script-with-service-account-3752f4e3df8c
This report is a workaround for reflecting the latest Google Apps Script to the deployed Web Apps without redeploying. https://gist.github.com/tanaikech/e46def22cf106b012dfa5ad359b93d24
Here, I would like to introduce the applications of Web Apps. If you can introduce your applications, please tell me. I would like to introduce them here.
This is a sample API for converting from a1Notation to GridRange. The GridRange is often used at Sheets API. Although I have thought that a1Notation is easy to use, there are no methods for converting it. So I created this as an API. I would like to introduce this as a sample for this report.
The sample script is as follows. At first, please copy and paste it to new standalone project.
function doGet(e) {
var output = {};
if (e.parameter.sheetid && e.parameter.a1notation) {
output = {
result: a1notation2gridrange1(
e.parameter.sheetid,
e.parameter.a1notation
),
};
} else {
output = { error: "Wrong parameters." };
}
return ContentService.createTextOutput(JSON.stringify(output));
}
// https://gist.github.com/tanaikech/95c7cd650837f33a564babcaf013cae0
function a1notation2gridrange1(sheetid, a1notation) {
try {
var data = a1notation.match(/(^.+)!(.+):(.+$)/);
var ss = SpreadsheetApp.openById(sheetid).getSheetByName(data[1]);
var range = ss.getRange(data[2] + ":" + data[3]);
var gridRange = {
sheetId: ss.getSheetId(),
startRowIndex: range.getRow() - 1,
endRowIndex: range.getRow() - 1 + range.getNumRows(),
startColumnIndex: range.getColumn() - 1,
endColumnIndex: range.getColumn() - 1 + range.getNumColumns(),
};
if (!data[2].match(/[0-9]/)) delete gridRange.startRowIndex;
if (!data[3].match(/[0-9]/)) delete gridRange.endRowIndex;
return gridRange;
} catch (e) {
return e;
}
}
And then, please deploy this as Web Apps. The condition is as follows.
- "Execute the app as:" : User accessing the web app
- "Who has access to the app:": Only myself
By this condition, owner and each client user can use Spreadsheets on own Google Drive. Before you access to Web Apps, please don't forget to share the project to users. The usage is as follows. This is a curl sample. But of course, you can also use this API using Google Apps Script.
curl -GL \
-H "Authorization: Bearer ### your access token ###" \
-d "sheetid=### spreadsheet ID ###" \
-d "a1notation=Sheet1%21A1%3aB2" \
"https://script.google.com/macros/s/#####/exec"
When you use this, please do URL encode for the range.
https://stackoverflow.com/questions/47833728/url-to-upload-to-google-drive
https://stackoverflow.com/questions/48917258/how-to-get-a-data-range-of-the-sheet
Here, I would like to introduce the sample situations with Web Apps. Those are threads of Stackoverflow. You can see the actual situations using Web Apps from them.
- How to upload a file via POST (doPost) to a Google Script's Web App?
- How to use importXML function with a file from Google Drive?
- Retrieving the output of a Google Apps script to local computer
- URL to upload to Google Drive
- Using AuthToken obtained via ScriptApp.getAuthToken() to call web apps in GAS
- Is there any limit on number of concurrent hits on Google App Script Web App
- Uploading files to pre-set Google Drive account using API?
- can't open Slack dialog through google apps scripts
- using an http request to update google spreadsheet
- Trying to use dev mode with GAS doPOST
- Google Sheets View Only Protection for User Running App Script
- How to assign UrlFetchApp with basic authorization to button?
- Protecting Cells of Spreadsheet that Users Copied from Your Google Drive to User's Google Drive using Google Apps Script
- Maintaining protected ranges while copying a file
- How to create webhook from Google Apps Script using the “exec” url rather than “dev” url. Exec returns 403 Error
- One Time Download for Google Drive
- Open a HTML file with data using Google Script
- Receiving Access-Control-Allow-Origin errors when submiting a form via Google Apps Script
- How to do curl POST of CSV data?
- How to POST from JS or node.js?
- Google Drive API: Create Folder: Allow conflict error if folder by name exists
- How to display classic Google Sites search box query in home page?
- Is it possible to insert a Horizontal Rule with Google Docs API?
- How to send an e.parameter to Google Apps Script using PHP and cURL
- Apps Script: Import and parse a HTML file from Drive to Sheets
- How to get all tab names/gid's with google.visualization from google spreadsheet?
- Google spreadsheet how to insert column after the last column
- Is it possible to set Chart options through the Google Sheets API?
- Upload a word doc and an google app script. Convert the word doc to a google doc and then run the app script clean up on it?
- Which are the right scopes to run request on Apps Script API
- google sheet REST API : how find location of cell with some value
- Convert Google Sheet to PDF and store in same Google Drive folder as sheet
- Can I download a Google Document, via the Google Docs API without an OAuth consent screen?
- Link to a specific sheet in published Google Sheet
- How can one write to a publicly available Google sheet (without authorization) in Python?
- Use path/slug after Web App's base url in Google Apps Script
- Is it possible to get fresh Google Slides presentation data in AppsScript?
- Error when running Youtube Data Service in App Scripts (js) – Daily Limit for Unauthenticated Use Exceeded
- Trouble accessing Google Sheet as a TSV file
- Get data from Google Sheets without Sheets API
- Retrieve text in namedRange with Google Docs API
- Using private sheets with tabletop.js
- I'm trying to get the direct download link for a file using the Google Drive API
- Upload file to my google drive with Google Apps Script (NO FORM IN GOOGLE)
- Upload image from URL to Google Drive through Google Drive API Javascript
- Google Sheets API with php - Find a cell value
- calling sheet function in GAS
- Possible to send mail from Google by URL?
- Formula Works in Google App Script Console, But Returns Authentication Error in Spreadsheets
- Scrape data from someone else's Google Sheets page using node.js?
- How to enable not authorized users to protect the spreadsheet
- Retrieving All Values from All Sheets from URL of 2PACX- of Web Published Google Spreadsheet using Google Apps Script and Javascript
- Changing Owner of the Sheet irrespective of the duplicator
- Google Sheets - Run script with Sheet Protection
- User Runs Script for Range Protected by Owner using Google Apps Script
- How to authenticate programmatically when running an Apps Script Web App from another Apps Script project
- Using triggers with custom functions on Google Apps Script
- how upload image & text input to same Row to google sheets
- TextRotation angle is never set in Google Sheet API
- Create one folder for all files uploaded with google apps script and web app form
- Google sheets: is it possible to protect a range of cell such that it can only be modify with a script?
- Uploading photo using Google Apps Script
- How do I get/create a hyperlink in only part of a cell?
- How do I get sorted results from the Google Photos search API?
- Google Script HTML form from Library throws error Uncaught
- Import XML content from a cell – Google spreadsheets
- How to update Chart Title via Google Sheet API (python)
- How to serve (PDF,binary) file from GAS web to a browser?
- How do I create an export to Google Sheet button on a web page?
- How to get raw numbers from Google Sheets API
- How to access data on different Google Spreadsheet through Google Apps Script for user without Editor access?
- I need to make the end user able to run some functions without google apps script without giving him access to the code
- Restrict Editors to Specific Ranges Script
- How to POST JSON to a Google Apps Script with DEV Link
- Creating an onEdit trigger on a sheet owned by a service account
- Is there a way where I can minimize execution time of a google apps script through triggers?
- If LockService works for a common script for multiple users, is there an option when dealing with multiple scripts sending data to the same column?
- Convert Google doc to Word document with reduced oAuth scopes
- How to use Web App approach when having functions run by editors on protected ranges, using Google Apps Scritps?
- google apps script web app call differents functions
- Multiple bulk import entries using Google Apps Script
- How to add a status bar for file upload? (Google Drive/ Apps Script)
- Inserting row into google sheet via external WebApp
- Need to make simple read and update webapp using appscript
- Can Google App Scripts create Web Apps with Multiple User Accounts?
- How to add a list of hyperlinks in a CSV field to a cell in Google Sheets?
- Dynamic hyperlink & Text in the same cell with a formula in google sheets
- Getting JavaScript file from Google Sheets
This repository has been created on April 26, 2018. After this repository was created, the information has continued to be grown now.