あるプロジェクトで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)
とは、オリジン間リソース共有
と呼ばれるブラウザのセキュリティ機能の一つです。
異なるオリジン間におけるリソースの共有を制限して信頼できるオリジンの対してのリクエストのみ応えるようにできます。
GASでの仕様
公式な声明が見つからなかったですが、GASは一応CORSの許可オリジンはワイルドカードにしており、すべてのオリジンからのアクセスを許可しているそうです。
加えてContent-Type:application/json
ではなく、x-www-form-urlencoded
で送信することで解決するという情報が見つかりました。
Content-Type:application/json
だとリクエストできない理由としては、プリフライトリクエスト
が関係しています。
プリフライトリクエストとは
プリフライトリクエストとは、CORSの仕様でサーバー側が認可しているメソッドかどうかの確認を特定のメソッドとヘッダーを利用して行います。
プリフライトリクエストの例を下に載せてみました。
HTTPメソッドは、OPTION
HTTPヘッダ-は、Access-Control-Request-Method
、Access-Control-Request-Headers
、Origin
の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-urlencoded
やtext/plain
は古くからあるContent-Type
であるためCORSの対象に入っていないことが考えられます。
しかしPostman等で送信すると上手くいく
Postman等のAPI送信ができるツールができるとステータスコード200が返ってきており、サーバーへのリクエストが上手くいっています。
これはCORSがブラウザとサーバー間の通信で必須となるセキュリティ機能の一つであるためです。
そのためブラウザ以外でのセキュリティ機能として存在しているわけではないため、リクエストがうまく届きます。
解決方法
今回はNext.jsでアプリケーションを作成していたため、サーバーコンポーネントとして実行することでブラウザではなく、サーバー間で通信させることで解決しました。
コードは以下のようになりました。
"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関連の記事