2022-08-29に更新

connect-webを試そうとしたら405エラーでちょっとはまった話

背景

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 のエラーが返ってきている。

image.png

原因

このエラーの意味は「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を理解しないといけないと思った。

ツイッターでシェア
みんなに共有、忘れないようにメモ

すずしめ

巫女見習いです。

Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?

コメント