connect-webを試そうとしたらCORSまわりでちょっとはまった話
作成日:
最終更新日:
背景
gRPCがフロントエンド通信の第一の選択肢になる時代がやってきたかも? | フューチャー技術ブログ を読んで、「HTTP/1.1で動いてcurlで投げたJSONも処理できるgRPC(もどき)って最強じゃん!」と思ったので チュートリアル を試してみた。
connect-go のほうのチュートリアルは自分で書いたprotocol bufferからサーバーとクライアントを両方作る方法の解説だったのだけれど、connect-web のほうのチュートリアルは既存のWebサービスにつなぐためのクライアントの作り方の解説であったため、チュートリアルを参考にしつつ connect-go で作ったサーバーに接続する connect-web のクライアントを作ろうとした。
本文
やろうとしたこと
まずは Getting started | Connect のPrepareの節を行い、プロジェクトをつくる。
次に、protocol bufferからTypeScriptのコードを生成。Generating code | Connect にある通りの buf.gen.yaml
を作った後、
ln -s ../connect-go-example/greet greet
buf generate
で gen
以下にTypeScriptのコードを生成する。
そして、App.tsx を以下のようにした。
import {
createConnectTransport,
createPromiseClient,
} from "@bufbuild/connect-web";
import React, { useState } from "react";
import { GreetService } from "../gen/greet/v1/greet_connectweb";
import './App.css'
const transport = createConnectTransport({
baseUrl: "http://localhost:8080",
})
const client = createPromiseClient(GreetService, transport)
function App() {
const [userName, setUserName] = useState("");
const [message, setMessage] = useState("ここにメッセージが入ります");
const sendName = async (e: React.FormEvent<HTMLElement>) => {
e.preventDefault();
const res = await client.greet({
name: userName,
})
setMessage(res.greeting)
}
return (
<div className="App">
<p>{message}</p>
<form onSubmit={sendName}>
<input value={userName} onChange={e => setUserName(e.target.value)} />
<button type="submit">送信</button>
</form>
</div>
)
}
export default App
これで npm run dev
して表示されたブラウザで「送信」ボタンを押すといいかんじに名前が表示されるはず……だが、何も出ない。ブラウザの開発者コンソールを見ると、405 Method Not Allowed のエラーが返ってきている。
原因
このエラーの意味は「POSTしか受け付けないサーバーにOPTIONSのリクエストを送っている」というものである。が、OPTIONSなんて聞いたこともないようなHTTPリクエストを送っているつもりはない。それでしばらく調べたのだが、これはCORSプリフライトというやつらしい。「単純リクエスト」以外のリクエストを送りたい時は予めOPTIONSで宛先サーバーの様子を調べるのである。
対処
というわけで、このOPTIONSをちゃんと処理できるような処理をサーバー側に足せば良いということになる。Goの net/http
の場合は rs/cors: Go net/http configurable handler to handle CORS requests という定番ライブラリがあるようなので、これを噛ませてみることにする。
package main
import (
"context"
greetv1 "example/gen/greet/v1"
"example/gen/greet/v1/greetv1connect"
"fmt"
"log"
"net/http"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"github.com/bufbuild/connect-go"
"github.com/rs/cors"
)
type GreetServer struct{}
func (s *GreetServer) Greet(
ctx context.Context,
req *connect.Request[greetv1.GreetRequest],
) (*connect.Response[greetv1.GreetResponse], error) {
log.Println("Request headers: ", req.Header())
res := connect.NewResponse(&greetv1.GreetResponse{
Greeting: fmt.Sprintf("Hello, %s!", req.Msg.Name),
})
res.Header().Set("Greet-Version", "v1")
return res, nil
}
func main() {
greeter := &GreetServer{}
mux := http.NewServeMux()
path, handler := greetv1connect.NewGreetServiceHandler(greeter)
mux.Handle(path, handler)
corsHandler := cors.Default().Handler(h2c.NewHandler(mux, &http2.Server{})) // corsのハンドラを追加した
// corsHandler := h2c.NewHandler(mux, &http2.Server{}) // もとの実装はこれ
http.ListenAndServe(
"localhost:8080",
corsHandler,
)
}
これでサーバーを立て直すと、無事リクエストが処理されるようになった。
感想
Connect自体とは関係ない現代の基本的なWeb技術のところではまってしまった。そろそろちゃんとCORSを理解しないといけないと思った。