🪪
Ginで最小CORS対応:フロントとAPIをつなぐ実践ガイド
1. 最小API(ベース)
まずは CORS を入れる前のベース API。
(※ すでに動いている方は読み飛ばしてOK)
下記のブログ記事にて取り上げたコードです。
該当コード
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type Memo struct {
ID uint `gorm:"primaryKey" json:"id"`
Content string `gorm:"not null" json:"content"`
}
var db *gorm.DB
func main() {
var err error
// SQLite データベースに接続。ファイルが存在しない場合は自動生成される
db, err = gorm.Open(sqlite.Open("memo.db"), &gorm.Config{})
if err != nil {
log.Fatal("failed to connect database:", err)
}
// 自動マイグレーションにより、 Memo モデルに基づいたテーブルを作成
db.AutoMigrate(&Memo{})
// エンドポイントの定義
// /memo では GET と POST を処理
http.HandleFunc("/memo", memoHandler)
// /memo/ 以降は DELETE 用など、URLパスにIDを含むリクエストを処理
http.HandleFunc("/memo/", memoHandlerWithID)
fmt.Println("Server is running on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
// memoHandler は、 GETリクエストで一覧表示、POSTリクエストでメモの追加を行う
func memoHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
listMemos(w, r)
case "POST":
addMemo(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
// memoHandlerWithID は、URLパスからIDを取得し、DELETEリクエストでメモを削除する
func memoHandlerWithID(w http.ResponseWriter, r *http.Request) {
if r.Method != "DELETE" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// URL例: /memo/3 から "3" を抽出
idStr := r.URL.Path[len("/memo/"):]
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest)
return
}
deleteMemo(w, r, uint(id))
}
func addMemo(w http.ResponseWriter, r *http.Request) {
var memo Memo
// リクエストボディからJSONをデコードして memo に格納する
if err := json.NewDecoder(r.Body).Decode(&memo); err != nil {
// JSON のパースに失敗した場合、 400 Bad Request を返す
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
result := db.Create(&memo)
if result.Error != nil {
// DB操作中にエラーが発生した場合、500 Internal Server Error を返す
http.Error(w, result.Error.Error(), http.StatusInternalServerError)
return
}
// 成功時は、 201 Created のステータスコードと共に、作成されたメモの情報を返す
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(memo)
}
func listMemos(w http.ResponseWriter, _ *http.Request) {
var memos []Memo
result := db.Find(&memos)
if result.Error != nil {
http.Error(w, result.Error.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(memos)
}
func deleteMemo(w http.ResponseWriter, _ *http.Request, id uint) {
result := db.Delete(&Memo{}, id)
if result.Error != nil {
http.Error(w, result.Error.Error(), http.StatusInternalServerError)
return
}
if result.RowsAffected == 0 {
// 指定したIDのメモが存在しない場合、 404 Not Found を返す
http.Error(w, "No memo found with given ID", http.StatusNotFound)
return
}
// 削除成功時は 200 OK で成功メッセージを返す
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "Memo deleted successfully")
}
2. CORS を正しく有効化する
2-1. 依存追加
% go get github.com/gin-contrib/cors
2-2. ミドルウェアを差し込む
import (
"time"
"github.com/gin-contrib/cors"
// ほかの import は省略
)
main() で ルートを登録する前 に CORS を差し込むのがポイントです。
router := gin.Default()
// CORS 設定
router.Use(cors.New(cors.Config{
AllowOrigins: []string{"http://localhost:5173"}, // 許可するフロントエンド
AllowMethods: []string{"GET", "POST", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * time.Hour, // プリフライト結果をキャッシュ
}))
続いてルート登録:
router.POST("/memo", addMemoHandler)
router.GET("/memo", listMemosHandler)
router.DELETE("/memo/:id", deleteMemoHandler)
log.Fatal(router.Run(":8080"))
3. 各設定項目の意味と落とし穴
CORS は「どのオリジンから、どのメソッドで、どのヘッダーを使ってアクセスできるか」をサーバーが明示し、ブラウザがそれを守る仕組みです。上記の各項目は以下の通り:
AllowOrigins
- 例: []string{"http://localhost:5173"}
- 許可するオリジンを列挙。完全一致のみ(ワイルドカードは * だが、Credentials と併用不可)。
- フロントの URL と一致しないと失敗します。
AllowMethods
- 例: []string{"GET", "POST", "DELETE", "OPTIONS"}
- 許可するHTTPメソッド。
- ブラウザは「これから POST 送るけどいい?」と プリフライトで確認します。
AllowHeaders
- 例: []string{"Origin", "Content-Type"}
- クライアントが 実際の本リクエストで送る追加ヘッダーを許可。
- JSON を送るなら Content-Type: application/json をここに含める。
ExposeHeaders
- 例: []string{"Content-Length"}
- レスポンスヘッダーのうち、フロントJSから読めるものを追加解禁。
- これを指定しないと、JSから Content-Length 等が見えません。
AllowCredentials
- 例: true
- fetch(..., { credentials: "include" }) のような Cookie/認証情報の送受信を許可。
- これを true にしたら、AllowOrigins に * は使えません(仕様)。
MaxAge(=プリフライト結果のキャッシュ)
- 例: 12 * time.Hour
- プリフライト(後述) の結果をブラウザ側で何時間キャッシュして良いか を秒数で示す(Ginのtime.Durationで指定→秒に変換されます)。
- 値が大きいほど、同一オリジン・同一メソッド・同一ヘッダーの組み合わせで、次からはプリフライトを省略できる→速くなる。
- ただし API 仕様を頻繁に変える環境では、キャッシュが効きすぎて 古い許可設定が残ることに注意。
4. そもそも プリフライト って何?
- ブラウザは安全のため、実リクエスト前に “確認” を投げる場合があります。
- それが OPTIONS メソッドによる プリフライトリクエスト(Preflight Request)です。
- 例:「Origin: http://localhost:5173 から POST して良い? Content-Type: application/json 使うけどOK?」
サーバーは「OKなら Access-Control-Allow-* ヘッダー」で回答します。 - OK なら、その後に本リクエスト(POST /memo 等)が送られます。
- MaxAge により、この “OK” の結果を ブラウザがキャッシュ→同条件なら次回以降プリフライト省略。
いつ発生する?
- GET でも、カスタムヘッダーを積む等でプリフライトが走る場合あり
- POST で application/json を送るのも “プリフライト対象” です
5. curl で CORS を正しく検証する
CORS は 異なるオリジンから叩いたときだけ効きます。
バックエンドに直接 curl すると CORS は関係ないので、Origin ヘッダーを明示します。
5-1. 本リクエスト(例:GET /memo)
curl -i -H "Origin: http://localhost:5173" http://localhost:8080/memo
期待するレスポンス例:
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Expose-Headers: Content-Length
Content-Type: application/json; charset=utf-8
Vary: Origin
Date: Sun, 14 Sep 2025 12:24:19 GMT
Content-Length: 2
[]%
5-2. プリフライト(OPTIONS)
curl -i -X OPTIONS http://localhost:8080/memo \
-H "Origin: http://localhost:5173" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type"
期待レスポンス例:
HTTP/1.1 204 No Content
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Origin,Content-Type
Access-Control-Allow-Methods: GET,POST,DELETE,OPTIONS
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Max-Age: 43200
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Date: Sun, 14 Sep 2025 12:41:43 GMT
6. まとめ
- CORS は サーバーが “この条件だけ許可” と宣言、ブラウザが その宣言を厳守する仕組み
- Gin の gin-contrib/cors を使えば 数行で正しく設定できる
- プリフライト(OPTIONS) は、本リクエスト前の 安全確認。MaxAge で キャッシュし高速化
- 検証は curl -H "Origin: ..." と OPTIONS を使うのが確実。レスポンスヘッダーを目視しよう
Discussion