0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GASでPOSTした時にCORSエラーが出てしまう時の対処法[Next.js, GAS]

Last updated at Posted at 2025-04-18

あるプロジェクトでNext.jsとGASでWebアプリケーションを作っていたのですが、あるエラーに遭遇したのでその解決策を備忘録として残しておこうと思います。

GASで出てしまったエラー

あるランキングの新規データ登録用の管理者画面を作ろうとして下のようなコードになっていました。
下のようなフォームを送信しようとしていました。

export const apiClient = axios.create({
    baseURL: process.env.NEXT_PUBLIC_GAS_URL,
    headers:{
        'Content-Type': 'application/json',
    }
})

const AdminPage = () => {
    const form = useForm<z.infer<typeof formSchema>>({
        resolver: zodResolver(formSchema),
        defaultValues: {
            name: "",
            avatar: "panda",
            score: "0",
        },
    })

    const onSubmit = (data: z.infer<typeof formSchema>) =>{
        apiClient.post("", {
            name: data?.name,
            animal: data?.avatar,
            score: Number(data?.score),
        }).then((res)=>{
            console.log(res.data)
        }).catch((err)=>{
            console.error(err);
        })
    }
    return(
        <div >
        <div className="my-6 text-center text-green-600">管理画面</div>
        <Card className="m-auto w-2xl">
            <CardHeader>
                <CardTitle className="text-center">新規レコード登録</CardTitle>
                <CardContent>
                    <Form {...form}>
                        <form onSubmit={form.handleSubmit(onSubmit)}>
                            <>
                    <div className="grid gap-4 sm:grid-cols-2">
                        <div className="grid gap-2">
                            <FormField
                                control={form.control}
                                name="name"
                                render={({ field }) => (
                                    <FormItem>
                                        <FormLabel>名前</FormLabel>
                                    <FormControl>

                                        <Input placeholder="プレイヤー名" {...field} />
                                    </FormControl>
                                        <FormMessage />
                                    </FormItem>
                                )}
                            />
                        </div>

                        <div className="grid gap-2">
                            <FormField
                                control={form.control}
                                name={`avatar`}
                                render={({ field }) => (
                                    <div>
                                        <FormItem>
                                        <FormLabel>
                                            アバター <span className="text-red-500">*</span>
                                        </FormLabel>
                                        <Select onValueChange={field.onChange} defaultValue={field.value}>
                                            <FormControl>
                                                <SelectTrigger>
                                                    <SelectValue placeholder="アバターを選択" />
                                                </SelectTrigger>
                                            </FormControl>
                                            <SelectContent>
                                                <SelectItem value="panda">🐼</SelectItem>
                                                <SelectItem value="bird">🐦</SelectItem>
                                                <SelectItem value="rabbit">🐰</SelectItem>
                                                <SelectItem value="mouse">🐭</SelectItem>
                                            </SelectContent>
                                        </Select>
                                            <FormMessage />
                                            </FormItem>
                                    </div>
                                )}
                            />
                        </div>

                        <div className="grid gap-2">
                            <FormField
                                control={form.control}
                                name="score"
                                render={({ field }) => (
                                        <FormItem>
                                            <FormLabel>スコア</FormLabel>
                                            <FormControl>
                                                <Input
                                                    type="number"
                                                    {...field}
                                                />
                                            </FormControl>
                                            <FormMessage />
                                        </FormItem>
                                )}
                            />
                        </div>
                    </div>
                    <div className="flex my-5 justify-center items-center">
                        <Button className="bg-green-600 hover:bg-green-700" type="submit">
                            <Save className="mr-2 h-4 w-4" />
                            プレイヤーを登録する
                        </Button>
                    </div>
                            </>
                        </form>
                    </Form>
                </CardContent>
            </CardHeader>
        </Card>
        </div>
    )
}

export default AdminPage;

GASのコードは下のようにdoPost関数でpostリクエストで処理できるようにしていました。

function doPost(e) {
  try {
    const req = JSON.parse(e.postData.contents)

    const ss = SpreadsheetApp.getActive();
    const sheet = ss.getSheetByName('シート1');


    const maxRow = sheet.getLastRow();


    sheet.appendRow([
      maxRow,
      req.name,
      req.animal,
      req.score.toString(),
      new Date().toISOString().split('T')[0]
    ])

    return ContentService.createTextOutput(JSON.stringify({
      status: 'success',
      message: 'データが正常に保存されました'
    })).setMimeType(ContentService.MimeType.JSON);

  } catch (error) {
    return ContentService.createTextOutput(JSON.stringify({
      status: 'error',
      code: 500,
      message: error.toString()
    }))
      .setMimeType(ContentService.MimeType.JSON)
      .setResponseCode(500);
  }
}

上の状態でformの送信ボタンを押した時に下のようなエラーが出てしまいました。

admin:1 Access to XMLHttpRequest at 'https://script.google.com/macros'
from origin 'http://172.31.193.145:3000' has been blocked by CORS policy: 
Response to preflight request doesn't pass access control check: 
No 'Access-Control-Allow-Origin' header is present on the requested resource.

エラーの原因

上のエラーはCORSによるエラーでした。

そもそもCORSとは

エラー文にあるCORS(Cross Origin Resource Sharing)とは、オリジン間リソース共有と呼ばれるブラウザのセキュリティ機能の一つです。

異なるオリジン間におけるリソースの共有を制限して信頼できるオリジンの対してのリクエストのみ応えるようにできます。

オリジンとは

http://example.com:3000https://api.example.comのような、プロトコルドメイン名ポートなどを組み合わせたものを指します。

GASでの仕様

公式な声明が見つからなかったですが、GASは一応CORSの許可オリジンはワイルドカードにしており、すべてのオリジンからのアクセスを許可しているそうです。

加えてContent-Type:application/jsonではなく、x-www-form-urlencodedで送信することで解決するという情報が見つかりました。

Content-Type:application/jsonだとリクエストできない理由としては、プリフライトリクエストが関係しています。

プリフライトリクエストとは

プリフライトリクエストとは、CORSの仕様でサーバー側が認可しているメソッドかどうかの確認を特定のメソッドとヘッダーを利用して行います。

プリフライトリクエストの例を下に載せてみました。
HTTPメソッドは、OPTION
HTTPヘッダ-は、Access-Control-Request-MethodAccess-Control-Request-HeadersOriginの3つが必要になります。

OPTIONS /resource/foo
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Origin, X-Requested-With
Origin: https://foo.bar.org

サーバー側がCORSの設定で許可をしているとレスポンスは下のようになります。
許可されていると許可内容が含められています。

HTTP/1.1 204 No Content
Connection: keep-alive
Access-Control-Allow-Origin: https://foo.bar.org
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
Access-Control-Allow-Headers: Origin, X-Requested-With
Access-Control-Max-Age: 86400

実際のプリフライトリクエストは下のようになっていました。

OPTIONS 
access-control-request-method: POST
access-control-request-headers: content-type
origin: http://172.31.193.145:3000

Content-Type:application/jsonでプリフライトリクエストが行われるのは、比較的新しいContent-Typeであり、application/x-www-form-urlencodedtext/plainは古くからあるContent-TypeであるためCORSの対象に入っていないことが考えられます。

しかしPostman等で送信すると上手くいく

Postman等のAPI送信ができるツールができるとステータスコード200が返ってきており、サーバーへのリクエストが上手くいっています。

これはCORSがブラウザとサーバー間の通信で必須となるセキュリティ機能の一つであるためです。
そのためブラウザ以外でのセキュリティ機能として存在しているわけではないため、リクエストがうまく届きます。

解決方法

今回はNext.jsでアプリケーションを作成していたため、サーバーコンポーネントとして実行することでブラウザではなく、サーバー間で通信させることで解決しました。

サーバーコンポーネント

Reactの機能で、ブラウザで表示するコンポーネントとはまた別でサーバーでコンポーネントを生成、フロントでそのコンポーネントを呼び出すことができる。

https://ja.react.dev/reference/rsc/use-server

コードは以下のようになりました。

"use server"

import {apiClient} from "@/lib/apiClient";

export interface NewRankingData {
    name?: string;
    avatar?: string;
    score?: string;
}

export const postRanking = async (reqData: NewRankingData) => {
    apiClient.post("", {
        name: reqData?.name,
        animal: reqData?.avatar,
        score: Number(reqData?.score),
    }).then((res)=>{
        console.log(res.data)
    }).catch((err)=>{
        console.error(err);
    })
}

このようにサーバー側から叩くことでGASのCORSエラーは回避することができます。

Next.jsの関連記事

GAS関連の記事

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?