ウェブアプリとしてデプロイしたGASをブラウザからAPIとして呼ぶ際のCORSエラー

gcp

GASで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-Typeapplication/x-www-form-urlencodedmultipart/form-datatext/plainのいずれかで送る必要がある。 Request.modeno-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