ウェブアプリとしてデプロイしたGASをブラウザからAPIとして呼ぶ際のCORSエラー
gcpGASでAPIを公開する方法として、scripts.run APIで実行できる実行可能APIと、ウェブアプリがある。 前者は認証が走り実行者の権限で動くが、後者は認証を行わずデプロイユーザーの権限で動かすこともできるのでパブリックなAPIとして使うことができる。 当然、不正な操作が行われないように注意する必要があり、GASのQuotaやhard limitも気にする必要がある。無料で運用することができるが、レイテンシやエラーハンドリング、監視などを考えるとやや心許ない。
ウェブアプリではdoGet(e)
とdoPost(e)
を実装することでそれぞれのメソッドのリクエストをハンドリングできる。これ以外のメソッドには対応していない。
function doGet(e) {
return ContentService.createTextOutput(JSON.stringify(e.parameter))
}
function doPost(e) {
return ContentService.createTextOutput(e.postData.contents)
}
claspでデプロイする際は一度画面上でウェブアプリとして公開した後、そのDeplymentsを更新するとそのまま反映できる。ただしDeploymentsに紐づくVersionに限りがあるので一旦undeployしている。
claspでGoogle Apps Scriptをローカルで開発しデプロイする - sambaiz-net
$ clasp push && clasp undeploy --all && clasp deploy -V 1
ウェブアプリのURLにリクエストを送ると https://script.googleusercontent.com/macros/echo?user_content_key=****
にリダイレクトするので、curlでリクエストする際は-L
フラグを付ける必要がある。
また、POSTする際は-X POST
を付けるとステータスコードに関わらずリダイレクト先にPOSTでリクエストし、Not Foundとなってしまうので付けない。
$ curl -L https://script.google.com/macros/s/*****/exec?aaa=bbb
{"aaa":"bbb"}
$ $ curl -d 'aaaa' -L https://script.google.com/macros/s/*****/exec
"aaaa"
GETとPOSTしか対応してないということは、OPTIONSメソッドによるCORSのpreflightリクエストも送れないということになる。
したがって、preflightリクエストが送られないように、余分なHeaderを付けずに Content-Type
をapplication/x-www-form-urlencoded
、multipart/form-data
、text/plain
のいずれかで送る必要がある。
Request.modeをno-cors
にすると送られるHeaderが制限されてエラーにはならなくなるが、レスポンスをスクリプトから取ることができない。
$ cat test.html
<script>
(async () => {
const resp = await fetch('https://script.google.com/macros/s/*****/exec', {
method: 'POST',
body: JSON.stringify({"req": 1})
})
console.log(`1: ${await resp.text()}`) // OK
})();
(async () => {
const resp = await fetch('https://script.google.com/macros/s/*****/exec', {
method: 'POST',
body: JSON.stringify({"req": 2}),
headers: {
'Content-Type': 'text/plain'
}
})
console.log(`2: ${await resp.text()}`) // OK
})();
(async () => {
const resp = await fetch('https://script.google.com/macros/s/*****/exec', {
method: 'POST',
body: JSON.stringify({"req": 3}),
headers: {
'Content-Type': 'application/json'
}
})
console.log(`3: ${await resp.text()}`) // CORS error
})();
(async () => {
const resp = await fetch('https://script.google.com/macros/s/*****/exec', {
method: 'POST',
mode: 'no-cors',
body: JSON.stringify({"req": 4}),
headers: {
'Content-Type': 'application/json'
}
})
console.log(`4: ${await resp.text()}`) // OK but text() is empty
})();
</script>
$ npx http-server .
$ open http://127.0.0.1:8080/test.html