- 投稿日:2020-10-18T21:33:41+09:00
gRPCサーバーから"unknown service"エラーが返ってきた時の対処方法について
最近流行りのgRPC.
スキーマ言語であるProtocol Buffersによってデータをシリアライズ化する事ができ、
これまで主流だったjson通信よりもより高速に通信を行えるとして、
マイクロサービス開発で採用されるケースが増えている。この間gRPCで構築したクライアント・サーバー間の通信をやろうとした時にタイトルの通りのバグが発生し
だいぶ手こずったので、忘備録として原因と解決策を残しておこうと思う。ソースコード
ソースはこちら。
https://github.com/yzmw1213/PostServiceやろうとした事
goで書いたgRPCサーバーで、以下2つのサービスを実装する。
- 投稿サービス
- 投稿につけるタグの管理サービス(マスタデータとしての扱い)そして、typescriptで実装している client側のコードから、上のサービスにリクエストを送り処理を行う。
client.tsimport { Tag, CreateTagRequest } from "~/grpc/tag_pb" import { TagServiceClient } from "~/grpc/TagServiceClientPb" post() { const client = new TagServiceClient( "http://localhost:8080", {}, {} ) const request = new CreateTagRequest() var tag = new Tag() tag.setTagId(postTag.tagID) tag.setTagName(postTag.tagName) tag.setStatus(postTag.status) request.setTag(tag) // TagServiceのcreateTagメソッドにリクエストを送る client.createTag(request, {}, (err, res) => { if (err != null) { console.log(err) } console.log(res) }) }上記のようにして、クライアント側からタグサービスにタグ作成のリクエストを行った際、次のエラーが起こった。
{ code: 12, message: "unknown service post_grpc.TagService" }解決策
code: 12 は何の事や...と思って公式のgitを見てみた。
すると、
// HTTP Mapping: 501 Not Implemented
UNIMPLEMENTED = 12;要するに、
「そのサービス、実装されてないで」 っていう意味の内容だった。そこで、呼び出しを行っているサービスがgRPCサーバーに登録されているかどうか確認する。
grpcサーバーの動作確認にはgrpcurlを使うといい。
この記事に色々詳しい事が書かれていた。
https://qiita.com/yukina-ge/items/a84693f01f3f0edba482例えばポート50051番でgRPCサーバーを構築しているとすると、以下のように叩くと良い。
# ポートに登録されているサービスの一覧 $ grpcurl -plaintext localhost:50051 list grpc.reflection.v1alpha.ServerReflection post_grpc.PostServiceServerReflectionとPostServiceは登録されているようだ。
あれ、じゃあTagServiceは...と思ってサーバー側のコード読んだら明らかなミスに気がついた。server.gopackage grpc import ( "fmt" "log" "net" "os" "os/signal" "github.com/yzmw1213/PostService/grpc/post_grpc" "github.com/yzmw1213/PostService/usecase/interactor" "google.golang.org/grpc" "google.golang.org/grpc/reflection" ) type server struct { PostUsecase interactor.PostInteractor TagUsecase interactor.TagInteractor } // NewPostGrpcServer gRPCサーバー起動 func NewPostGrpcServer() { lis, err := net.Listen("tcp", "0.0.0.0:50051") if err != nil { log.Fatalf("Failed to listen: %v", err) } server := &server{} s := makeServer() // PostServiceを serverに登録 post_grpc.RegisterPostServiceServer(s, server) // TagServiceの登録が抜けている!!! // Register reflection service on gRPC server. reflection.Register(s) log.Println("main grpc server has started") go func() { if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }() ch := make(chan os.Signal, 1) signal.Notify(ch, os.Interrupt) // Block until a sgnal is received <-ch fmt.Println("Stopping the server") s.Stop() fmt.Println("Closing the client") lis.Close() fmt.Println("End of Program") } func makeServer() *grpc.Server { s := grpc.NewServer( grpc.UnaryInterceptor(grpc.UnaryServerInterceptor(transmitStatusInterceptor)), ) return s }post_grpc.RegisterPostServiceServer で、PostServiceを登録はしているが
TagServiceも同様にgRPCサーバーに登録しなければならない。次のコードを追加し、解決した。
server.go// PostServiceを serverに登録 post_grpc.RegisterPostServiceServer(s, server) // 以下を追加 // タグサービス登録 post_grpc.RegisterTagServiceServer(s, server)振り返り
今回、原因の特定にかなり時間を費やしてしまった。
clientとserverの間にenvoy Proxyを置いており、proxyの方に気を取られまくっていたので。。
悩んであれこれ試した割にはだいぶ初歩的なミスだった...。
grpcurl、これからは初手から使っていこう。補足
マイクロサービス運用を想定しているgRPCサーバーの構築では、
1 service / 1 server の前提で書かれている記事が数多い印象があるが、必ずしも
全てのサービス毎に細くサーバーを分ける分ける必要もなく、上で実装しているように
それぞれRegisterServiceすれば運用に支障は無いと思っている。関連性の高いサービス(たとえば、ユーザー登録サービスと認証サービスとか)は
この様に同一サーバーでの運用することが十分可能だと思う。今後、認証サービスも書く予定なので試してみようと思う。
参考記事
- 投稿日:2020-10-18T20:30:58+09:00
【Go】実行・ビルド・パッケージのテストについて
一応 前提環境
・macOS
・goenv 2.0.0beta11
・go version go1.15.2 darwin/amd64実行方法
今回は定番のHello Worldを実行します。
とりあえず$GOPATH直下にhello.goという名前でファイルを作成し、そこに下記のプログラムを書きます。
package main //パッケージの宣言 import ( //使用するパッケージをインポート "fmt" ) func main() { //関数main(エントリーポイント)の定義 fmt.Println("Hello World") }そうしたらファイルを作成したディレクトリをターミナルで開き、
$ go run hello.go
を入力し、Hello Worldと表示されたら実行成功です。ビルド
次はhello.goを実行ファイル形式にコンパイルします。
下記のようにbuildコマンドを入力、-oオプションを使用することで実行ファイル名を指定できます。
$ go build -o hello hello.goするとhelloという実行ファイルができるので、ターミナルで下記のコマンドを入力するだけでHello Worldが実行できます。
$ ./hello Hello Worldパッケージのテスト
仮に下記のようなパッケージ構成になっているとして、testsディレクトリの配下のファイルたちをテストするとします。
また、依存モジュール管理ツール Modulesを使用しており、
go mod init github.com/username/testproject
にしてあります。
※go 1.13から取り込まれていますが、go 1.11から移行期ではありますが、export GO111MODULE=on
にすることで使えるようになります。testproject │────── tests │ │────testA │ │────testB │ │────testC │ │────── main.goまず、testsディレクトリの配下に末尾が_test.go終わるようにファイルを作成します。これはパッケージをテストする際に決められたルールです。
例)tests_test.go
下記のようにtests_test.goファイルの内容を書きます。
package tests import ( "testing" ) func TestJapaneseSubject(t *testing.T) { expect := "Japanese" actual := JapaneseSubject() if expect != actual { t.Errorf("%s != %s", expect, actual) } } func TestEnglishSubject(t *testing.T) { expect := "English" actual := EnglishSubject() if expect != actual { t.Errorf("%s != %s", expect, actual) } } func TestMathSubject(t *testing.T) { expect := "Math" actual := MathSubject() if expect != actual { t.Errorf("%s != %s", expect, actual) } }それではターミナルでコマンドを入力してテストを実行し、下記のように出力されれば成功です。
$ go test github.com/username/testproject/tests ok github.com/noa-1129/testproject/tests 0.506sまた、-vオプションをつけるとファイルごとに詳細が確認できます。
$ go test -v github.com/username/testproject/tests === RUN TestJapaneseSubject --- PASS: TestJapaneseSubject (0.00s) === RUN TestEnglishSubject --- PASS: TestEnglishSubject (0.00s) === RUN TestMathSubject --- PASS: TestMathSubject (0.00s) PASS ok github.com/username/testproject/tests 0.230s最後に
今回パッケージのテストでは依存モジュール管理ツールとしてGo Modulesを使ってテストをしましたが、Go modulesについては次の記事で書こうと思います!
- 投稿日:2020-10-18T19:07:15+09:00
Goには非短絡評価演算子がなかった
あまり意識することはなかったのですが、躓いてしまったのでメモ。
package, import文は省略しているので、完全なコードはリンク先をご確認ください。TL;DR
Goには非短絡評価演算子がないため、関数を含む場合は
- 事前に評価するか、
- 関数を左辺に記述すべし
躓いた点
以下のようなコードがあるとします。
https://play.golang.org/p/en49-fRSNLU
func main() { funcCalled := true funcCalled = funcCalled || returnFalse() fmt.Println(funcCalled) } func returnFalse() bool { fmt.Println("returnFalse() called") return false }true Program exited.ここで
returnFalse()
を毎回 call したかったのですが、"returnFalse() called"
が出力されることはありませんでした。これは
||
が短絡評価であり、左辺がtrue
の場合に右辺は評価されずに処理が完了するためです。よって
funcCalled = funcCalled || returnFalse()
は
funcCalled := true
のため- 右辺の
returnFalse()
が評価されずに処理を完了します解決策
returnFalse()
を call するには、一般的には
- 事前に関数をcallして評価しておく
funcCalled = returnFalse() || funcCalled
のように、関数callを先に記述する- 短絡評価ではない論理演算子
|
を使用するのいずれかに修正します。
最初に1の、事前に関数をcallして評価しておく場合。
https://play.golang.org/p/EKTl6cQqsmx
func main() { funcCalled := true result := returnFalse() // 事前に評価 funcCalled = result || funcCalled fmt.Println(funcCalled) } func returnFalse() bool { fmt.Println("returnFalse() called") return false }returnFalse() called true Program exited.OKですね。
次に2の、関数callを先に記述した場合。
https://play.golang.org/p/Iq2jqGVE1j7
func main() { funcCalled := true funcCalled = returnFalse() || funcCalled // 関数callを先に記述 fmt.Println(funcCalled) } func returnFalse() bool { fmt.Println("returnFalse() called") return false }returnFalse() called true Program exited.良さそうです。
最後に3の短絡評価ではない演算子を使った場合。
https://play.golang.org/p/aNMVkYBw7LI
func main() { funcCalled := true funcCalled = funcCalled | returnFalse() // 短絡評価ではない演算子を使う fmt.Println(funcCalled) } func returnFalse() bool { fmt.Println("returnFalse() called") return false }./prog.go:9:26: invalid operation: funcCalled | returnFalse() (operator | not defined on bool) Go build failed.エラーとなってしまいました。定義されていないのでしょうか。
言語仕様を見てみましょう。
https://golang.org/ref/spec#Logical_operators
Logical operators apply to boolean values and yield a result of the same type as the operands. The right operand is evaluated conditionally.
&& conditional AND p && q is "if p then q else false" || conditional OR p || q is "if p then true else q" ! NOT !p is "not p"定義されていないようでした。
結論
Goには非短絡評価演算子がないため、関数を含む場合は
- 事前に評価するか、
- 関数を左辺に記述すべし
- 投稿日:2020-10-18T19:07:15+09:00
Goには非短絡評価の論理演算子がなかった
あまり意識することはなかったのですが、躓いてしまったのでメモ。
package, import文は省略しているので、完全なコードはリンク先をご確認ください。以下のコードのGoのバージョンは
go1.14.9
です。TL;DR
Goには非短絡評価の論理演算子がないため、関数を含む場合は
- 事前に評価するか、
- 関数を左辺に記述すべし
躓いた点
以下のようなコードがあるとします。
https://play.golang.org/p/en49-fRSNLU
func main() { funcCalled := true funcCalled = funcCalled || returnFalse() fmt.Println(funcCalled) } func returnFalse() bool { fmt.Println("returnFalse() called") return false }true Program exited.ここで
returnFalse()
を毎回 call したかったのですが、"returnFalse() called"
が出力されることはありませんでした。これは
||
が短絡評価であり、左辺がtrue
の場合に右辺は評価されずに処理が完了するためです。よって
funcCalled = funcCalled || returnFalse()
は
funcCalled := true
のため- 右辺の
returnFalse()
が評価されずに処理を完了します解決策
returnFalse()
を call するには、一般的には
- 事前に関数をcallして評価しておく
funcCalled = returnFalse() || funcCalled
のように、関数callを先に記述する- 短絡評価ではない論理演算子
|
を使用するのいずれかに修正します。
最初に1の、事前に関数をcallして評価しておく場合。
https://play.golang.org/p/EKTl6cQqsmx
func main() { funcCalled := true result := returnFalse() // 事前に評価 funcCalled = result || funcCalled fmt.Println(funcCalled) } func returnFalse() bool { fmt.Println("returnFalse() called") return false }returnFalse() called true Program exited.OKですね。
次に2の、関数callを先に記述した場合。
https://play.golang.org/p/Iq2jqGVE1j7
func main() { funcCalled := true funcCalled = returnFalse() || funcCalled // 関数callを先に記述 fmt.Println(funcCalled) } func returnFalse() bool { fmt.Println("returnFalse() called") return false }returnFalse() called true Program exited.良さそうです。
最後に3の短絡評価ではない論理演算子
|
を使った場合。https://play.golang.org/p/aNMVkYBw7LI
func main() { funcCalled := true funcCalled = funcCalled | returnFalse() // 短絡評価ではない演算子を使う fmt.Println(funcCalled) } func returnFalse() bool { fmt.Println("returnFalse() called") return false }./prog.go:9:26: invalid operation: funcCalled | returnFalse() (operator | not defined on bool) Go build failed.エラーとなってしまいました。定義されていないのでしょうか。
言語仕様を見てみましょう。
https://golang.org/ref/spec#Logical_operators
Logical operators apply to boolean values and yield a result of the same type as the operands. The right operand is evaluated conditionally.
&& conditional AND p && q is "if p then q else false" || conditional OR p || q is "if p then true else q" ! NOT !p is "not p"定義されていないようでした。
結論
Goには非短絡評価の論理演算子がないため、関数を含む場合は
- 事前に評価するか、
- 関数を左辺に記述すべし
- 投稿日:2020-10-18T19:07:15+09:00
Goには完全評価演算子がなかった
あまり意識することはなかったのですが、躓いてしまったのでメモ。
package, import文は省略しているので、完全なコードはリンク先をご確認ください。TL;DR
Goには完全評価論理演算子がないため、関数を含む場合は
- 事前に評価するか、
- 関数を左辺に記述すべし
躓いた点
以下のようなコードがあるとします。
https://play.golang.org/p/en49-fRSNLU
func main() { funcCalled := true funcCalled = funcCalled || returnFalse() fmt.Println(funcCalled) } func returnFalse() bool { fmt.Println("returnFalse() called") return false }true Program exited.ここで
returnFalse()
を毎回 call したかったのですが、"returnFalse() called"
が出力されることはありませんでした。これは
||
が短絡評価であり、左辺がtrue
の場合に右辺は評価されずに処理が完了するためです。よって
funcCalled = funcCalled || returnFalse()
は
funcCalled := true
のため- 右辺の
returnFalse()
が評価されずに処理を完了します解決策
returnFalse()
を call するには、一般的には
- 事前に関数をcallして評価しておく
funcCalled = returnFalse() || funcCalled
のように、関数callを先に記述する- 完全評価を行う論理演算子
|
を使用するのいずれかに修正します。
最初に1の、事前に関数をcallして評価しておく場合。
https://play.golang.org/p/EKTl6cQqsmx
func main() { funcCalled := true result := returnFalse() // 事前に評価 funcCalled = result || funcCalled fmt.Println(funcCalled) } func returnFalse() bool { fmt.Println("returnFalse() called") return false }returnFalse() called true Program exited.OKですね。
次に2の、関数callを先に記述した場合。
https://play.golang.org/p/Iq2jqGVE1j7
func main() { funcCalled := true funcCalled = returnFalse() || funcCalled // 関数callを先に記述 fmt.Println(funcCalled) } func returnFalse() bool { fmt.Println("returnFalse() called") return false }returnFalse() called true Program exited.良さそうです。
最後に3の完全評価演算子を使った場合。
https://play.golang.org/p/aNMVkYBw7LI
func main() { funcCalled := true funcCalled = funcCalled | returnFalse() // 完全評価演算子を使う fmt.Println(funcCalled) } func returnFalse() bool { fmt.Println("returnFalse() called") return false }./prog.go:9:26: invalid operation: funcCalled | returnFalse() (operator | not defined on bool) Go build failed.エラーとなってしまいました。定義されていないのでしょうか。
言語仕様を見てみましょう。
https://golang.org/ref/spec#Logical_operators
Logical operators apply to boolean values and yield a result of the same type as the operands. The right operand is evaluated conditionally.
&& conditional AND p && q is "if p then q else false" || conditional OR p || q is "if p then true else q" ! NOT !p is "not p"定義されていないようでした。
結論
Goには完全評価論理演算子がないため、関数を含む場合は
- 事前に評価するか、
- 関数を左辺に記述すべし
- 投稿日:2020-10-18T14:35:38+09:00
go言語学習雑記 1
ちょっと前にGo言語を勉強していて、備忘録がてらその時のメモを書いていきます。
パッケージ
goはパッケージと言う単位で構成され、その中の関数や変数の単位でimport,exportをしていきます。
できるかどうかは、文字の大文字小文字で判別されています。package main import ( "fmt" "math" ) func main() { fmt.Println(math.Pi) //OK fmt.Println(math.pi) // error }大文字の場合はexport されず
小文字の場合はexport されます。変数宣言
変数は var <変数名> <型名> の形で宣言されます。
また分割代入っぽくかけます。セイウチ演算子(:=)を使うと型推論を効かせて型名を省略して宣言できます。その場合はの宣言は <変数名> := <値> となります
定数は const <変数名> = <値> となります。 定数の場合は勝手に型推論が働きます。
var i, j int = 1, 2 test := "string" // 型推論を効かせて変数初期化 const Pi = 3.14 // 定数、この場合型推論を効かせて定数初期化はできない基本型
go は zero valueと言う機能があり、特定の型で値が代入されなかった場合自動的に値が代入されます。
なので、値を入れ忘れてがnullになってると言うケースを防ぐことができます。bool // zero value はfalse string // zero value は "" int int8 int16 int32 int64 // zero value は0 uint uint8 uint16 uint32 uint64 uintptr byte // uint8 の別名 rune // int32 の別名 // Unicode のコードポイントを表す float32 float64 complex64 complex128なお、なぜか基本型に複素数があります。
型変換
<型名>(<変数名>)で型変換ができる
var i int = 42 var f float64 = float64(i) var u uint = uint(f)
- 投稿日:2020-10-18T13:55:35+09:00
golangでbashを呼び出す
コマンド結果を受け取らず、実行完了まで待機
- Run
package main import ( "fmt" "os/exec" ) func main() { err := exec.Command("pwd").Run() if err != nil { fmt.Println("Command Exec Error.") } }コマンド結果を受け取る
- Output
package main import ( "fmt" "os/exec" ) func main() { out, err := exec.Command("pwd").Output() if err != nil { fmt.Println("Command Exec Error.") } // 実行したコマンドの結果を出力 fmt.Printf("pwd result: %s", string(out)) }標準出力エラーを受け取る
- CombinedOutput package main import ( "fmt" "os/exec" ) func main() { out, err := exec.Command("ls", "dummy").CombinedOutput() if err != nil { fmt.Println("Command Exec Error.") } // 実行したコマンドの標準出力+標準エラー出力の内容 fmt.Printf("ls result: \n%s", string(out)) }コマンド実行終了を待たない
- start
- waitとの組み合わせで待つことができる
package main import ( "fmt" "os/exec" ) func main() { cmd := exec.Command("sleep", "10") fmt.Println("Command Start.") err := cmd.Start() if err != nil { fmt.Println("Command Exec Error.") } //コメント外すと待機する //cmd.Wait() fmt.Println("Command Exit.") }参考
- 投稿日:2020-10-18T13:47:04+09:00
go-templateはチューリング完全?brainf*ck処理系を作ってみた
TL;DR
- go-templateの標準関数/構文だけでbrainf*ck処理系を実装!
- kubectlをbrainf*ckインタープリターとして利用(パワーワード)
- podのマニフェストにbrainf*ckコード記載
- kubectlでpod情報取得時に、brainf*ckインタープリターのgo-templateにかける
- リポジトリ: go-template-bf-interpreter
- 実用性は...
はじめに
C++のテンプレート、Pythonの内包表記、...有り余る自由度がもたらす黒魔術により、単体でチューリング完全になってしまった言語機能があります。
そして、ここにgo-templateが黒魔術の仲間入りを果たしたことを宣言します。
go-templateといえば、
gin
のHTMLやkubectl
のレスポンス成形等のテンプレート用ミニ言語というイメージが強いですが(というか名前的にそういう意図で作られていますが)、以下のことが全て可能です1。
range
ブロックによるコレクションのループif
ブロックによる条件分岐- 変数の宣言、更新(再代入)
参考(公式):template - GoDoc
というわけで、チューリング完全性の登竜門、brainf*ckの処理系をgo-template(Go言語に頼らず、標準関数/構文だけ2)で実装してみました。
動作方法
kubectl
を使用します。podのマニフェストファイルにbrainf*ckのソースコードを入れ、
kubectl
で取得する際にgo-templateを利用し成形(=go-templateのbrainf*ckインタープリターで評価)するという仕組みをとっています。リポジトリ:go-template-bf-interpreter
(
kubectl
を使用したのは、kubectl
ではじめてgo-templateを知ったからです。冒頭で「)gin
のHTML生成でおなじみ」などと書いていましたがエアプですすみませんマニフェストファイルにbrainf*ckソースコードを格納
metadata.annotations
には任意のキーと値(文字列)を格納できるのでここにソースコードを格納します。ついでにbrainf*ckへの標準入力も入れておきましょう。hello.yamlmetadata: name: bf-source-pod # add dummies to make annotations long enough to loop (see bf-interpreter.tpl for details) annotations: # used for bf stdin input: "" # bf source code src: > +++++++++[>++++++++>+++++++++++>+++>+<<<<-]>.>++.+++++++..+++. >+++++.<<+++++++++++++++.>.+++.------.--------.>+.>+. dummy1: dummy1 dummy2: dummy2 #...ちなみに、ダミーのキーを大量に入れているのは、インタープリターのループ回数を稼ぐためです(後述)。
helloworldのコードは Brainfuck 超入門 - Qiita のものを使用させていただきました。
podのコンテナは
どうせ使わないので何でもいいです。とりあえず起動が速いalpineイメージにしておきました。実行の流れ
# k8sクラスターを構築 (例はkind) $ kind create cluster # 上記のhelloworldコードが入ったpodを作成 $ kubectl create -f hello.yaml pod/bf-source-pod created # pod情報(=ソースコード)を取得し、その内容をインタープリターとなるgo-templateで評価 $ kubectl get pods -o go-template-file=bf-interpreter.tpl Hello World!go-templateプログラミングデザインパターン(?)
brainf*ckインタープリターの実装はこんな感じです。
インデント地獄。
bf-interpreter.tpl以下、使用した小技について紹介していきます。
空白を詰める
{{}}
の両端に-
を付けると、かっこの外側の空白を詰めることが出来ます。これを使えば、{{}}
の外側でインデント、改行を入れてもすべて無視されます。
go-templateプログラミングでは可読性のために必須です。付けないとワンライナー縛りが始まります。
- ハイフンなし
withspace.tpl{{if true}} {{println "got it!"}} {{else}} {{println "no..."}} {{end}}無駄なスペースがそのまま出力されるkubectl get pods -o go-template-file=withspace.tpl got it!
- ハイフンあり
trimspace.tpl{{- if true -}} {{- println "got it!" -}} {{- else -}} {{- println "no..." -}} {{- end -}}無駄なスペースは消える$ kubectl get pods -o go-template-file=trimspace.tpl got it!ループ
brainf*ckにはwhileループが必要です。ソースコード各文字のパースや
[
,]
評価時のジャンプに使用します。しかし、残念ながら
string
はrange
でイテレーションできません。
さらに、go-templateで作成できるリテラルは定数のみなので、配列やマップを新たに作ることもできません。$ kubectl get pods -o go-template --template '{{range $c := "abc"}}{{println $c}}{{end}}' ... error: error executing template "{{range $c := \"abc\"}}{{println $c}}{{end}}": template: output:1:14: executing "output" at <"abc">: range can't iterate over abcそこで、pod情報の
metadata.annotations
(map[string]string
)をrange
でのループに使用します。アノテーションにダミーを混ぜて、16回ループできるようにしています。hello.yamlmetadata: name: bf-source-pod annotations: # used for bf stdin input: "" # bf source code src: > +++++++++[>++++++++>+++++++++++>+++>+<<<<-]>.>++.+++++++..+++. >+++++.<<+++++++++++++++.>.+++.------.--------.>+.>+. dummy1: dummy1 dummy2: dummy2 #... dummy14: dummy14このループを多段に使うことで、メモリの初期化やソースコードのパースを行っています。
bf-interpreter.tpl{{- /* mapを代入し、rangeブロックに使用 */ -}} {{- $Looper := (index .items 0).metadata.annotations -}} {{- /* メモリの初期化 (len $Looper)^2バイトを0埋め) */ -}} {{- $memory := "" -}} {{- range $Looper -}} {{- range $Looper -}} {{- $memory = print $memory "\x00" -}} {{- end -}} {{- end -}} {{- /* ソースコードの読み込み (len $Looper)^3文字を頭からパース) */ -}} {{- range $Looper -}} {{- range $Looper -}} {{- range $Looper -}} {{- /* NOTE: exists is implemented only in k8s parser */ -}} {{- if exists $Source (len $parsingBytePos) -}} {{- $tokenByte := index $Source (len $parsingBytePos) -}} {{- $token := printf "%c" $tokenByte -}} {{- /* トークンを評価(省略) */ -}} {{- /* increment pos */ -}} {{- $parsingBytePos = print $parsingBytePos " " -}} {{- end -}} {{- end -}} {{- end -}} {{- end -}}ちなみに、16回ループしているのはインタープリターのスペックのキリを良くするためです。
- メモリサイズ:
256byte
($Looper
2段ループ)- パース可能ソースコード長上限:
4096文字
($Looper
3段ループ)加算、減算
残念ながら(2度目)、go-templateには整数の加算、減算の関数、演算子がありません。
しかし、brainf*ckにはメモリの値やポインタの更新の際加算、減算が必要です。そこで、文字列の長さを整数の代わりに使用します。
文字列は結合、スライシングにより長さを変えることができ、len
関数で長さを整数で取得できます。
- 加算
inc.tpl{{- /* go-templateのprintはGoのSprintに相当(副作用はない) */ -}} {{- $numStr := " " -}} {{- println (len $numStr) -}} {{- $numStr = print $numStr " " -}} {{- println (len $numStr) -}}$ kubectl get pods -o go-template-file=inc.tpl 1 2
- 減算
dec.tpl{{- $numStr := " " -}} {{- println (len $numStr) -}} {{- $numStr = slice $numStr 1 -}} {{- println (len $numStr) -}}$ kubectl get pods -o go-template-file=dec.tpl 1 0メモリの更新
前述の通りgo-templateでは配列を作成できません。また、既存オブジェクトの要素のみ更新することもできません。代入式の左辺値になれるのは変数のみです。
$ kubectl get pods -o go-template --template '{{(index .items 0) := "hoge"}}' error: error parsing template {{(index .items 0) := "hoge"}}, template: output:1: unexpected ":=" in operandそこで、文字列をメモリとして利用します。
Goでは文字列のインデックスを取る際文字列を[]byte
として扱うので、文字列自体をバイト列とみなすことが出来ます。go言語のindexs := "abc" fmt.Println([]byte(s)) // [97 98 99] fmt.Println(s[0]) // 97 fmt.Println([]byte(s)[0]) // 97そして、
+
や-
等で文字列中のあるバイトのみ更新する場合は、「当該バイトのみ置き換えた新しいメモリ文字列」を作成しています。bf-interpreter.tpl{{- else if eq $token "+" -}} {{- /* ...参照アドレスの値を取り出しインクリメント(省略) */ -}} {{- /* メモリの更新 */ -}} {{- /* 参照アドレスのみ置き換えた新しいメモリで置き換える */ -}} {{- /* 参照アドレスより前のメモリ */ -}} {{- $former := slice $memory 0 (len $memoryPtr) -}} {{- /* 参照アドレスより後のメモリ */ -}} {{- /* NOTE: (len (print $memoryPtr " ") は参照アドレス+1 */ -}} {{- $latter := slice $memory (len (print $memoryPtr " ")) -}} {{- /* 置換(バイトの値をそのままprintすると整数が文字列化されてしまうので、printfで対応するアスキーコードの文字に変換) */ -}} {{- $memory = print $former (printf "%c" $incrementedValue) $latter -}} {{- end -}}おわりに
以上、go-templateのbrainf*ck処理系の紹介でした。
Let's go-templateプログラミング!
- 投稿日:2020-10-18T01:25:02+09:00
GraphQL(gqlgen)エラーハンドリング
お題
Type-safe GraphQL for Go
を謳うGolang製GraphQLライブラリであるgqlgenを使って、GraphQL Server側のエラーハンドリングについて検討。想定する読者
- Golangについてある程度書ける。
- 「GraphQL is 何?」ではない。
- gqlgenの getting-started で初期セットアップくらいはやったことがある。
関連記事索引
- 第10回「GraphQL(gqlgen)エラーハンドリング」
- 第9回「GraphQLにおける認証認可事例(Auth0 RBAC仕立て)」
- 第8回「GraphQL/Nuxt.js(TypeScript/Vuetify/Apollo)/Golang(gqlgen)/Google Cloud Storageの組み合わせで動画ファイルアップロード実装例」
- 第7回「GraphQLにおけるRelayスタイルによるページング実装(後編:フロントエンド)」
- 第6回「GraphQLにおけるRelayスタイルによるページング実装(前編:バックエンド)」
- 第5回「DB接続付きGraphQLサーバ(by Golang)をローカルマシン上でDockerコンテナ起動」
- 第4回「graphql-codegenでフロントエンドをGraphQLスキーマファースト」
- 第3回「go+gqlgenでGraphQLサーバを作る(GORM使ってDB接続)」
- 第2回「NuxtJS(with Apollo)のTypeScript対応」
- 第1回「frontendに「nuxtjs/apollo」、backendに「go+gqlgen」の組み合わせでGraphQLサービスを作る」
開発環境
# OS - Linux(Ubuntu)
$ cat /etc/os-release NAME="Ubuntu" VERSION="18.04.5 LTS (Bionic Beaver)"# バックエンド
# 言語 - Golang
$ go version go version go1.15.2 linux/amd64# gqlgen
v0.13.0IDE - Goland
GoLand 2020.2.3 Build #GO-202.7319.61, built on September 16, 2020今回の全ソース
https://github.com/sky0621/study-gqlgen/tree/v0.2
実践
gqlgenを使ってサーバーサイド側でどうGraphQLのエラーハンドリングをするべきか、いくつかの方法を試行してみる。
1.ベーシックな方法でハンドリング
いくつかのパターンを列挙する。
server.go
package main import ( "log" "net/http" "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/playground" "github.com/sky0621/study-gqlgen/errorhandling/graph" "github.com/sky0621/study-gqlgen/errorhandling/graph/generated" ) func main() { srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}})) http.Handle("/", playground.Handler("GraphQL playground", "/query")) http.Handle("/query", srv) log.Fatal(http.ListenAndServe(":8080", nil)) }schema.graphqls
type Query { normalReturn: [Todo!]! errorReturn: [Todo!]! customErrorReturn: [Todo!]! customErrorReturn2: [Todo!]! customErrorReturn3: [Todo!]! customErrorReturn4: [Todo!]! panicReturn: [Todo!]! } type Todo { id: ID! text: String! }リゾルバー
schema.resolvers.gopackage graph // This file will be automatically regenerated based on the schema, any resolver implementations // will be copied through when generating and any unknown code will be moved to the end. import ( "context" "errors" "fmt" "time" "github.com/99designs/gqlgen/graphql" "github.com/sky0621/study-gqlgen/errorhandling/graph/generated" "github.com/sky0621/study-gqlgen/errorhandling/graph/model" "github.com/vektah/gqlparser/v2/gqlerror" ) func (r *queryResolver) NormalReturn(ctx context.Context) ([]*model.Todo, error) { return []*model.Todo{ {ID: "001", Text: "something1"}, {ID: "002", Text: "something2"}, }, nil } func (r *queryResolver) ErrorReturn(ctx context.Context) ([]*model.Todo, error) { return nil, errors.New("error occurred") } func (r *queryResolver) CustomErrorReturn(ctx context.Context) ([]*model.Todo, error) { return nil, gqlerror.Errorf("custom error") } func (r *queryResolver) CustomErrorReturn2(ctx context.Context) ([]*model.Todo, error) { graphql.AddError(ctx, gqlerror.Errorf("add error")) graphql.AddErrorf(ctx, "add error2: %s", time.Now().String()) return nil, nil } func (r *queryResolver) CustomErrorReturn3(ctx context.Context) ([]*model.Todo, error) { return nil, &gqlerror.Error{ Extensions: map[string]interface{}{ "code": "A00001", "field": "text", "value": "トイレ掃除", }, } } func (r *queryResolver) CustomErrorReturn4(ctx context.Context) ([]*model.Todo, error) { return nil, &gqlerror.Error{ Extensions: map[string]interface{}{ "errors": []map[string]interface{}{ { "code": "A00001", "field": "text", "value": "トイレ掃除", }, { "code": "A00002", "field": "text", "value": "トイレ掃除", }, }, }, } } func (r *queryResolver) PanicReturn(ctx context.Context) ([]*model.Todo, error) { panic(fmt.Errorf("panic occurred")) } // Query returns generated.QueryResolver implementation. func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } type queryResolver struct{ *Resolver }パターン別の解説
参考
GraphQLにおけるレスポンスに関して。
正常系の場合、以下のような構造になる。{ "data": { 〜〜〜〜 } }リゾルバーで何かしらエラーを返すと、以下のような構造になる。
{ "errors": [ { "message": 〜〜〜〜, "path": [〜〜〜〜] } ], "data": null }その他、以下、参考にされたし。
https://gqlgen.com/reference/errors/■正常系
func (r *queryResolver) NormalReturn(ctx context.Context) ([]*model.Todo, error) { return []*model.Todo{ {ID: "001", Text: "something1"}, {ID: "002", Text: "something2"}, }, nil }■go標準のエラーを返すパターン
func (r *queryResolver) ErrorReturn(ctx context.Context) ([]*model.Todo, error) { return nil, errors.New("error occurred") }指定したエラーメッセージが
message
に積まれている。
path
は勝手に付与される。
■gqlgenが用意したメソッドを介してエラーを返すパターン
func (r *queryResolver) CustomErrorReturn(ctx context.Context) ([]*model.Todo, error) { return nil, gqlerror.Errorf("custom error") }やはり、指定したエラーメッセージが
message
に積まれている。
構造はgo標準のエラーを返すパターンと同じ。
■複数のエラーを返すパターン
func (r *queryResolver) CustomErrorReturn2(ctx context.Context) ([]*model.Todo, error) { graphql.AddError(ctx, gqlerror.Errorf("add error")) graphql.AddErrorf(ctx, "add error2: %s", time.Now().String()) return nil, nil }指定した2種類のエラーがそれぞれの
message
に積まれている。
これまでのエラー発生時と違い、data
がnull
ではなく空スライスが返されているのが、やや気になる。
(おそらくだけど、return
でerrorを返さなかったためかな。)
■個別拡張領域を使うパターン
func (r *queryResolver) CustomErrorReturn3(ctx context.Context) ([]*model.Todo, error) { return nil, &gqlerror.Error{ Extensions: map[string]interface{}{ "code": "A00001", "field": "text", "value": "トイレ掃除", }, } }
message
には何も積まず、用意されたextensions
にサービス固有の表現でエラー内容を定義する。
map[string]interface{}
なので任意の構造が使える。
これにより、レスポンスを受けたフロントエンド側でcode
に応じたエラーメッセージの生成とエンドユーザーへの表示などが可能になる。
■個別拡張領域を使うパターン2
func (r *queryResolver) CustomErrorReturn4(ctx context.Context) ([]*model.Todo, error) { return nil, &gqlerror.Error{ Extensions: map[string]interface{}{ "errors": []map[string]interface{}{ { "code": "A00001", "field": "text", "value": "トイレ掃除", }, { "code": "A00002", "field": "text", "value": "トイレ掃除", }, }, }, } }返したいエラーは1つとは限らない。このようにマップのスライスという形で保持すれば複数のエラーを返すことも当然可能。
■panicが発生した時のパターン
func (r *queryResolver) PanicReturn(ctx context.Context) ([]*model.Todo, error) { panic(fmt.Errorf("panic occurred")) }
panic
発生時に積まれたメッセージは無視してinternal system error
がmessage
には積まれる。
2.カスタマイズしたエラーハンドリング
よっぽど小規模なサービスでない限り、サービス固有のエラーハンドリング表現が必要になってくると思う。
gqlgenではhandler生成時に「エラー発生時」と「panic発生時」にフックして処理を追加する仕掛けがある。
この仕掛けを利用して、
リゾルバーからは(エラー発生時)サービス固有に定義したエラー構造体を返し、handlerでフックして、エラー構造体を加工してレスポンスとする実装をしてみる。schema.graphqls
type Query { errorPresenter: [Todo!]! panicHandler: [Todo!]! } type Todo { id: ID! text: String! }schema.resolvers.go
サービス固有のエラー構造体として
AppError
を作成。リゾルバーからはその構造体を返却。package graph import ( "context" "fmt" "github.com/sky0621/study-gqlgen/errorhandling2/graph/generated" "github.com/sky0621/study-gqlgen/errorhandling2/graph/model" ) type ErrorCode string const ( ErrorCodeRequired ErrorCode = "1001" ErrorCodeUnexpectedSituation ErrorCode = "9999" ) type AppError struct { Code ErrorCode Msg string } func (e AppError) Error() string { return fmt.Sprintf("[%s]%s", e.Code, e.Msg) } func (r *queryResolver) ErrorPresenter(ctx context.Context) ([]*model.Todo, error) { return nil, AppError{ Code: ErrorCodeRequired, Msg: "text is none", } } func (r *queryResolver) PanicHandler(ctx context.Context) ([]*model.Todo, error) { panic("unexpected situation") } // Query returns generated.QueryResolver implementation. func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } type queryResolver struct{ *Resolver }server.go
SetErrorPresenter()
でセットする関数の中で、リゾルバーで投げたエラーを受け取り、AppError
だったら*gqlerror.Error{}
の構造に編集し直している。
ちなみに、SetRecoverFunc()
も用意してpanic発生時もサービス固有の想定したエラー表現になるよう編集している。package main import ( "context" "errors" "log" "net/http" "github.com/99designs/gqlgen/graphql" "github.com/vektah/gqlparser/v2/gqlerror" "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/playground" "github.com/sky0621/study-gqlgen/errorhandling2/graph" "github.com/sky0621/study-gqlgen/errorhandling2/graph/generated" ) func main() { srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}})) srv.SetErrorPresenter(func(ctx context.Context, e error) *gqlerror.Error { err := graphql.DefaultErrorPresenter(ctx, e) var appErr graph.AppError if errors.As(err, &appErr) { return &gqlerror.Error{ Message: appErr.Msg, Extensions: map[string]interface{}{ "code": appErr.Code, }, } } return err }) srv.SetRecoverFunc(func(ctx context.Context, err interface{}) error { return &gqlerror.Error{ Extensions: map[string]interface{}{ "code": graph.ErrorCodeUnexpectedSituation, "cause": err, }, } }) http.Handle("/", playground.Handler("GraphQL playground", "/query")) http.Handle("/query", srv) log.Fatal(http.ListenAndServe(":8080", nil)) }動作確認
エラーハンドリング
うまいこと、エラーコードとエラーメッセージの振り分けが出来ている。
panicハンドリング
cause
としてpanic発生時のエラーを積むようにしたため、ちゃんとレスポンスに発生元のエラー内容も乗るようになった。
3.汎用性を考慮してカスタマイズしたエラーハンドリング
エラーと一口に言っても、バリデーションエラー、認証系のエラー、DB接続エラー、等々、いろんな種類があり、エラー構造として必要な要素も変わってくると思う。
単一のエラーで返せばいい場合もあれば、(バリデーションエラーのように)エラー要素1つ1つが必要で、結果として複数のエラーを返す必要がある場合もある。
こういった状況を踏まえ、なるべく汎用的にエラーハンドリングすることを試みる。サービス固有のエラー構造
apperror.gopackage graph import ( "context" "net/http" "github.com/vektah/gqlparser/v2/gqlerror" "github.com/99designs/gqlgen/graphql" ) type AppError struct { httpStatusCode int // http.StatusCodeXXXXXXX を入れる appErrorCode AppErrorCode // サービス固有に定義したエラーコード /* * 以下、全てのエラー表現に必須ではない要素(オプションとして設定可能) */ field string value string } func (e *AppError) AddGraphQLError(ctx context.Context) { extensions := map[string]interface{}{ "status_code": e.httpStatusCode, "error_code": e.appErrorCode, } if e.field != "" { extensions["field"] = e.field } if e.value != "" { extensions["value"] = e.value } graphql.AddError(ctx, &gqlerror.Error{ Message: "", Extensions: extensions, }) } func NewAppError(httpStatusCode int, appErrorCode AppErrorCode, opts ...AppErrorOption) *AppError { a := &AppError{ httpStatusCode: httpStatusCode, appErrorCode: appErrorCode, } for _, o := range opts { o(a) } return a } // 認証エラー用 func NewAuthenticationError(opts ...AppErrorOption) *AppError { return NewAppError(http.StatusUnauthorized, AppErrorCodeAuthenticationFailure, opts...) } // 認可エラー用 func NewAuthorizationError(opts ...AppErrorOption) *AppError { return NewAppError(http.StatusForbidden, AppErrorCodeAuthorizationFailure, opts...) } // バリデーションエラー用 func NewValidationError(field, value string, opts ...AppErrorOption) *AppError { options := []AppErrorOption{WithField(field), WithValue(value)} for _, opt := range opts { options = append(options, opt) } return NewAppError(http.StatusBadRequest, AppErrorCodeValidationFailure, options...) } // その他エラー用 func NewInternalServerError(opts ...AppErrorOption) *AppError { return NewAppError(http.StatusInternalServerError, AppErrorCodeUnexpectedFailure, opts...) } type AppErrorCode string // MEMO: サービスの定義によっては意味のある文字列よりもコード体系を決めるのもあり。 const ( // 認証エラー AppErrorCodeAuthenticationFailure AppErrorCode = "AUTHENTICATION_FAILURE" // 認可エラー AppErrorCodeAuthorizationFailure AppErrorCode = "AUTHORIZATION_FAILURE" // バリデーションエラー AppErrorCodeValidationFailure AppErrorCode = "VALIDATION_FAILURE" // その他の予期せぬエラー AppErrorCodeUnexpectedFailure AppErrorCode = "UNEXPECTED_FAILURE" ) type AppErrorOption func(*AppError) func WithField(v string) AppErrorOption { return func(a *AppError) { a.field = v } } func WithValue(v string) AppErrorOption { return func(a *AppError) { a.value = v } }解説
まず、サービス固有のエラー構造体として
AppError
を作成。
エラー要素として何を持つかはサービスによりまちまちだとは思うけど、とりあえず以下2つはエラーの内容問わず必須として定義。
- HTTPステータスコード
- サービス固有のエラーコード
type AppError struct { httpStatusCode int // http.StatusCodeXXXXXXX を入れる appErrorCode AppErrorCode // サービス固有に定義したエラーコード 〜〜 } func NewAppError(httpStatusCode int, appErrorCode AppErrorCode, opts ...AppErrorOption) *AppError { a := &AppError{ httpStatusCode: httpStatusCode, appErrorCode: appErrorCode, } 〜〜 }続いて、例えばバリデーションエラーのように「どのフィールドのどの値が」という情報も欲しくなるようなケースのために構造体内には(冗長でも)パターン別に必要な要素を持たせるようにする。
type AppError struct { 〜〜 /* * 以下、全てのエラー表現に必須ではない要素(オプションとして設定可能) */ field string value string }ただし、今後、こういった要素の追加が必要になるたびに
New
関数を修正(つまり、呼び元も全て修正)なんてしたくないので、Functional Option Patternを用いることにする。オプション適用のための関数を定義し、
New
関数では可変引数で渡す(つまり、ないならないでOK)ようにする。type AppErrorOption func(*AppError) func NewAppError(httpStatusCode int, appErrorCode AppErrorCode, opts ...AppErrorOption) *AppError { a := &AppError{ httpStatusCode: httpStatusCode, appErrorCode: appErrorCode, } for _, o := range opts { o(a) } return a }で、
AppErrorOption
の適用事例として以下2つを用意。func WithField(v string) AppErrorOption { return func(a *AppError) { a.field = v } } func WithValue(v string) AppErrorOption { return func(a *AppError) { a.value = v } }こうすることで、今後、エラー構造体に追加したい要素が増えても、既存の呼び出し元を修正する必要なく拡張できる。
(説明が雑なのが一番の理由だけど)初見でこの仕組みを理解するのは、けっこうキツいと思うので、「Functional Option Pattern」でググってもらって易しい解説記事を読んでもらいたい。。。
あとは、サービス固有のエラーコードを以下のように定義して、
type AppErrorCode string // MEMO: サービスの定義によっては意味のある文字列よりもコード体系を決めるのもあり。 const ( // 認証エラー AppErrorCodeAuthenticationFailure AppErrorCode = "AUTHENTICATION_FAILURE" // 認可エラー AppErrorCodeAuthorizationFailure AppErrorCode = "AUTHORIZATION_FAILURE" // バリデーションエラー AppErrorCodeValidationFailure AppErrorCode = "VALIDATION_FAILURE" // その他の予期せぬエラー AppErrorCodeUnexpectedFailure AppErrorCode = "UNEXPECTED_FAILURE" )エラーのタイプ別に専用の
New
関数でも用意してあげればOK。// 認証エラー用 func NewAuthenticationError(opts ...AppErrorOption) *AppError { return NewAppError(http.StatusUnauthorized, AppErrorCodeAuthenticationFailure, opts...) } // 認可エラー用 func NewAuthorizationError(opts ...AppErrorOption) *AppError { return NewAppError(http.StatusForbidden, AppErrorCodeAuthorizationFailure, opts...) } // バリデーションエラー用 func NewValidationError(field, value string, opts ...AppErrorOption) *AppError { options := []AppErrorOption{WithField(field), WithValue(value)} for _, opt := range opts { options = append(options, opt) } return NewAppError(http.StatusBadRequest, AppErrorCodeValidationFailure, options...) } // その他エラー用 func NewInternalServerError(opts ...AppErrorOption) *AppError { return NewAppError(http.StatusInternalServerError, AppErrorCodeUnexpectedFailure, opts...) }リゾルバー
試しに、タイプ別にエラーを生成してGraphQLエラーとして追加してあげるとこんな感じ。
(認証エラーなんかは当然、ユーザーIDなどを積んだりするだろうけど、とりあえずはサンプルなので。)schema.resolvers.gopackage graph import ( "context" "github.com/sky0621/study-gqlgen/errorhandling3/graph/generated" "github.com/sky0621/study-gqlgen/errorhandling3/graph/model" ) func (r *queryResolver) CustomErrorReturn(ctx context.Context) ([]*model.Todo, error) { // 認証エラーを追加 NewAuthenticationError().AddGraphQLError(ctx) // 認可エラーを追加 NewAuthorizationError().AddGraphQLError(ctx) // バリデーションエラーを追加 NewValidationError("name", "taro").AddGraphQLError(ctx) // その他のエラーを追加 NewInternalServerError().AddGraphQLError(ctx) return nil, nil } // Query returns generated.QueryResolver implementation. func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } type queryResolver struct{ *Resolver }schema.graphqls
type Query { customErrorReturn: [Todo!]! } type Todo { id: ID! text: String! }server.go
今回はhandlerへの仕込みは無し。
package main import ( "log" "net/http" "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/playground" "github.com/sky0621/study-gqlgen/errorhandling3/graph" "github.com/sky0621/study-gqlgen/errorhandling3/graph/generated" ) func main() { srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}})) http.Handle("/", playground.Handler("GraphQL playground", "/query")) http.Handle("/query", srv) log.Fatal(http.ListenAndServe(":8080", nil)) }動作確認
この通り、統一されたフォーマットになっているので、レスポンスを受け取る側でのハンドリングもしやすいはず。。。
まとめ
とりあえずエラーが返せればいいといったシンプルなものから、一応、汎用性を考慮してサービス固有のエラー構造を定義したやり方まで複数のエラーハンドリング案を提示してみた。
もちろん、ここにあるパターン以外にもあるだろうし、ここにあげたものはプロダクションレベルとしては心もとない。
1サービスとして考えるなら、ここで返したエラー内容をフロントエンドではどのようにハンドリングするかも重要な要素だと思う。