tag:crieit.net,2005:https://crieit.net/tags/Connect/feed 「Connect」の記事 - Crieit Crieitでタグ「Connect」に投稿された最近の記事 2024-05-02T23:53:49+09:00 https://crieit.net/tags/Connect/feed tag:crieit.net,2005:PublicArticle/18284 2022-08-20T12:35:05+09:00 2024-05-02T23:53:49+09:00 https://crieit.net/posts/connect-go-with-cors connect-webを試そうとしたらCORSまわりでちょっとはまった話 <h1 id="背景"><a href="#%E8%83%8C%E6%99%AF">背景</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://future-architect.github.io/articles/20220819a/">gRPCがフロントエンド通信の第一の選択肢になる時代がやってきたかも? | フューチャー技術ブログ</a> を読んで、「HTTP/1.1で動いてcurlで投げたJSONも処理できるgRPC(もどき)って最強じゃん!」と思ったので <a target="_blank" rel="nofollow noopener" href="https://connect.build/docs/introduction">チュートリアル</a> を試してみた。</p> <p>connect-go のほうのチュートリアルは自分で書いたprotocol bufferからサーバーとクライアントを両方作る方法の解説だったのだけれど、connect-web のほうのチュートリアルは既存のWebサービスにつなぐためのクライアントの作り方の解説であったため、チュートリアルを参考にしつつ connect-go で作ったサーバーに接続する connect-web のクライアントを作ろうとした。</p> <h1 id="本文"><a href="#%E6%9C%AC%E6%96%87">本文</a></h1> <h2 id="やろうとしたこと"><a href="#%E3%82%84%E3%82%8D%E3%81%86%E3%81%A8%E3%81%97%E3%81%9F%E3%81%93%E3%81%A8">やろうとしたこと</a></h2> <p>まずは <a target="_blank" rel="nofollow noopener" href="https://connect.build/docs/web/getting-started">Getting started | Connect</a> のPrepareの節を行い、プロジェクトをつくる。</p> <p>次に、protocol bufferからTypeScriptのコードを生成。<a target="_blank" rel="nofollow noopener" href="https://connect.build/docs/web/generating-code">Generating code | Connect</a> にある通りの <code>buf.gen.yaml</code> を作った後、</p> <pre><code>ln -s ../connect-go-example/greet greet buf generate </code></pre> <p>で <code>gen</code> 以下にTypeScriptのコードを生成する。</p> <p>そして、App.tsx を以下のようにした。</p> <pre><code class="javascript">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 </code></pre> <p>これで <code>npm run dev</code> して表示されたブラウザで「送信」ボタンを押すといいかんじに名前が表示されるはず……だが、何も出ない。ブラウザの開発者コンソールを見ると、<strong>405 Method Not Allowed</strong> のエラーが返ってきている。</p> <p><a href="https://crieit.now.sh/upload_images/cd71f0d0d2c7cca896698c0a4bd3a31d630051c1b137f.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/cd71f0d0d2c7cca896698c0a4bd3a31d630051c1b137f.png?mw=700" alt="image.png" /></a></p> <h2 id="原因"><a href="#%E5%8E%9F%E5%9B%A0">原因</a></h2> <p>このエラーの意味は「POSTしか受け付けないサーバーにOPTIONSのリクエストを送っている」というものである。が、OPTIONSなんて聞いたこともないようなHTTPリクエストを送っているつもりはない。それでしばらく調べたのだが、これはCORSプリフライトというやつらしい。「単純リクエスト」以外のリクエストを送りたい時は予めOPTIONSで宛先サーバーの様子を調べるのである。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/HTTP/CORS">オリジン間リソース共有 (CORS) - HTTP | MDN</a></li> </ul> <h2 id="対処"><a href="#%E5%AF%BE%E5%87%A6">対処</a></h2> <p>というわけで、このOPTIONSをちゃんと処理できるような処理をサーバー側に足せば良いということになる。Goの <code>net/http</code> の場合は <a target="_blank" rel="nofollow noopener" href="https://github.com/rs/cors">rs/cors: Go net/http configurable handler to handle CORS requests</a> という定番ライブラリがあるようなので、これを噛ませてみることにする。</p> <pre><code class="go">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, ) } </code></pre> <p>これでサーバーを立て直すと、無事リクエストが処理されるようになった。</p> <h1 id="感想"><a href="#%E6%84%9F%E6%83%B3">感想</a></h1> <p>Connect自体とは関係ない現代の基本的なWeb技術のところではまってしまった。そろそろちゃんとCORSを理解しないといけないと思った。</p> すずしめ