Echo(Go)のMiddlewareを理解する (Recover,CORS)

# はじめに
こんにちは。TuneCoreJapanでバックエンドエンジニアをしています @masafumi330 です。
この記事は[Wano Group Advent Calendar2023](https://qiita.com/advent-calendar/2023/wano-group) 最終日の記事です。もう一度言いますよ。最終日の記事です！

今回は、弊社バックエンドアプリケーションの開発でも使用しているGo、 そのフレームワーク**Echo**について、その**Middleware**の機能にフォーカスを絞り、
- Middlewareとは何なのか？
- Middlewareによってどのような課題を解決できるのか？
- 実際にどのようにMiddlewareの機能を使うのか？
をまとめていきたいと思います。

https://echo.labstack.com/

# 調査の経緯
まずは今回、EchoのMiddleware機能を調査してみるにあたった経緯を話そうと思います。

Wanoに入社して２年間、私はバックエンドチームにてGoを用いたアプリケーション開発を行ってきました。
通常の開発では、Middlewareを特に意識することなく、アプリケーションの核となるビジネスロジックに集中して開発することができています。
これはこれで、**「Middlewareを意識することなくアプリケーションを開発できる」** という理想的な状況だと思いますが、一方でMiddlewareの存在や効果を理解せずに使用していることに対する疑問が生まれました。
そこで、Middlewareについて深く理解しようという動機から、今回調べてみようと思った次第です。

# Echoとは？
Echoとは、[公式サイト](https://echo.labstack.com/docs#labstack-echo-project)より参照すると、GoによるWebアプリケーションを開発するために必要な様々なツールを提供してくれるフレームワークです。

> *The Echo project is a powerful and versatile web framework for building scalable and high-performance web applications in the Go programming language. It follows the principles of simplicity, flexibility, and performance to provide developers with an efficient toolkit for building robust web applications.*
> （訳）
> *Echoプロジェクトは、Goプログラミング言語でスケーラブルで高性能なWebアプリケーションを構築するための強力で多機能なWebフレームワークです。このプロジェクトは、シンプルさ、柔軟性、パフォーマンスの原則に従い、開発者に頑丈なWebアプリケーションを構築するための効果的なツールキットを提供します。*

軽量かつ柔軟でありながら、Webアプリケーションに必要なおおよその機能を備えています。
- ルーティング
- エラーハンドリング
- ロギング
- 認証
- etc.

EchoのようなWebアプリケーションフレームワークを使う利点は以下にあると思います。
- 開発速度向上（巨人の肩に乗る）
- 保守性向上（フレームワークに責務を一部委譲することにより、ビジネスロジックに集中できる）


# Middlewareとは？

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651150/49e11731-9f53-39ec-0205-2818331ceb6d.png)
*Image Credit: Sunrise Integration Blog*

この記事を読んで頂いている方ならばWebアプリケーションの世界で「Middleware」という言葉を聞いたことがあると思います。
Middlewareとは、その言葉の通り、様々なアプリケーションの**間**を取り持ち、異なる機能を連携させることを責務とする機構のことを指します。

アプリケーションが機能する上で、リクエストがサーバーに届いてからレスポンスがクライアントに返るまでの一連の流れには多くの処理が含まれます。この処理の中で、**セッション管理、エラーハンドリング、ログ**の取得など、**共通の課題を解決するための仕組み**がMiddlewareです。

ClientとApplicationの板挟みに合いながら日々奮闘する間取り持ち隊長。それが**Middleware**なのです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651150/9c0a1adf-50c8-336c-a9c1-3c817a4aa774.png)
*ClientとApplicationの板挟みで頑張るMiddlewareくん*


例えばEchoフレームワークでは、RecoverというMiddlewareの機能を使って、アプリケーション内で発生したエラーをキャッチし、プロセスを停止させないようにエラーから復帰することによって、可用性とその後の原因解析へと役立ちます。
また、Loggerはリクエストやレスポンスに関する情報をログとして残し、デバッグやトラブルシューティングに役立ちます。

外界の複雑さを吸収し、間を取り持ってくれるMiddleware...なんとありがたい機能でしょう。
私も今となっては大人になりましたが、子供の頃身のお世話をしてくれたり、毎日ご飯を作ってくれて学校に行かせてくれた親にはいくら感謝してもしきれません。（大人になって初めて親のありがたみを知る成人男性26歳）

# Middlewareを使ってみる
それでは実際に、EchoのMiddlewareツールを使っていきたいと思います。

## Recover
Recoverは、アプリケーション内で発生したpanicから復帰し、stack trace(エラー箇所までの経路) とエラーハンドリングまで導き、アプリケーション処理を継続するために使用します。

例として、"Hello World"を返してくれるようなエンドポイントがあるとします。

```go:main.go
func main() {
	e := echo.New()
	e.GET("/", func(c echo.Context) error {
		return c.String(http.StatusOK, "Hello, World!")
	})
	e.Logger.Fatal(e.Start(":8080"))
}
```

サーバーを起動した状態で、エンドポイントへリクエストを行うと、問題なくレスポンスが返ってきます。
```sh
$ go run main.go

$ curl localhost:8080/
Hello, World! 
```

しかし、処理の途中でpanicを発生させた場合、プロセスは処理継続不可能となりプロセスは停止。レスポンスは返ってこなくなってしまいます。

```go:main.go
func main() {
	e := echo.New()
	e.GET("/", func(c echo.Context) error {
        panic("panic!") // panicを発生させる
		return c.String(http.StatusOK, "Hello, World!")
	})
	e.Logger.Fatal(e.Start(":8080"))
}
```

```sh
$ go run main.go

$ curl localhost:8080/ 
curl: (52) Empty reply from server
$ curl localhost:8080/ 
curl: (52) Empty reply from server

# サーバーが停止したため、応答がなくなる。
```

```sh:実際のサーバーログ
echo: http: panic serving 127.0.0.1:62709: panic
goroutine 19 [running]:
net/http.(*conn).serve.func1()
        /usr/local/go/src/net/http/server.go:1850 +0xb0
panic({0x100c39ca0, 0x100c8c648})
        /usr/local/go/src/runtime/panic.go:890 +0x258
...
```

この問題を解決するため、実際にRecoverを導入してみましょう。

```go:main.go
func main() {
	e := echo.New()
	e.Use(middleware.Recover()) // Recoverを導入
	e.GET("/", func(c echo.Context) error {
		panic("panic") // panicを発生させる
		return c.String(http.StatusOK, "Hello, World!")
	})
	e.Logger.Fatal(e.Start(":1323"))
}
```

Echoでは、Middlewareは付け外し可能なブロックのように、
```go
e.Use(/*middleware*/)
e.Use(/*middleware*/)
...
```
とchainさせることができます。Recoverの場合は、
`e.Use(middleware.Recover())`とchainするだけで導入が可能です。

再度サーバーを起動してpanicを発生させてみましょう。

```sh
$ go run main.go

$ curl localhost:8080/ 
{"message":"Internal Server Error"}
$ curl localhost:8080/ 
{"message":"Internal Server Error"}
```

panicが発生してもInternal Server Errorとして500HTTPレスポンスで処理し、エラーを返しています。
また、再度リクエストを送ってもレスポンスがあることにより、サーバーが停止していないことが分かります。

サンプルのように、ローカルで起動させている簡単なプログラムであれば、再度サーバーを起動させるだけですが、Webサービスのような大規模なアプリケーションであれば、**サーバーが停止してしまうのは重大なインシデント**です。

そのために、Recoverのように、panicが起きてもまずは復帰しサービスの継続を計るフェイルセーフ機構があります。

## CORS
Cross-Origin Resource Sharing（CORS）は、異なるオリジンからのリクエストを許可するための仕組みです。EchoのCORSミドルウェアを使用することで、セキュアかつ柔軟なAPI通信を実現できます。

下記では、FEサーバーとBEサーバーを立て、FEサーバーからBEサーバーにリクエストを送り、情報の取得を行うような例を考えてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651150/7f3b160a-bce6-9d6f-2539-29f66720ca33.png)


### CORS導入前
GET /hello を送ると、Jsonでhello worldのメッセージを返してくれるようなAPIを定義します。

BEサーバー
```go:main.go
func main() {
	e := echo.New()

	e.GET("/hello", func(c echo.Context) error {
		return c.JSON(http.StatusOK, map[string]string{"message": "Hello World"})
	})
	e.Logger.Fatal(e.Start(":1323"))
}
```

curlで確認すると、問題ないようです。

APIテスト
```sh
$ curl localhost:8080/hello -i 
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Date: Sun, 17 Dec 2023 03:48:09 GMT
Content-Length: 26

{"message":"Hello World"}
```

次にFEサーバーを立てます。
ユースケースとしては、ボタンを押したら、GET /helloリクエストを送り、返ってきたレスポンスの内容を描画するようなケースを考えてみます。

構築は、Next.jsのボイラーテンプレートから実装します。

FEサーバー
```sh
$ npx create-next-app frontend
$ cd frontend
```

```javascript:pages/index.js
import { useState } from 'react';

const Home = () => {
  const [response, setResponse] = useState('');

  const fetchData = async () => {
    try {
      const res = await fetch('http://localhost:8080/hello');
      const data = await res.json();
      setResponse(data.message);
    } catch (error) {
      console.error('Error fetching data:', error);
    }
  };

  return (
    <div>
      <h1>CORS Test</h1>
      <button onClick={fetchData}>Send GET /hello Request</button>
      {response && <p>Response from server: {response}</p>}
    </div>
  );
};

export default Home;
```

```json:package.json
// ポート指定
"scripts": {
    "dev": "next dev -p 8079",
}
```

実際の画面は以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651150/128e9e19-fa61-23d5-f500-a7179a6d5e11.png)

早速ボタンを叩いてリクエストを送ってみます。
すると、なにも反応がありません。
検証ツールを確認してみると、どうやら CORSエラー　というエラーが発生しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651150/9d6fa1c5-b741-0b52-0868-1157d0c4b987.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651150/b6464dd3-7084-66c2-18e5-a86c23cb14f1.png)


原因
- BEサーバーとしてリクエストを許可するオリジンに、FEサーバー (http://localhost:8079) を許可していないため。


### CORS導入後
それでは次にCORS Middlewareを導入します。
リクエスト可能なオリジンとして、FEサーバーを許可します。

```go:main.go
func main() {
	e := echo.New()

	// CORS middleware
	e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
		AllowOrigins: []string{"http://localhost:8079"},
		AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept},
	}))

	e.GET("/hello", func(c echo.Context) error {
		return c.JSON(http.StatusOK, map[string]string{"message": "Hello World"})
	})
	e.Logger.Fatal(e.Start(":8080"))
}
```

再度ボタンを押してリクエストを送ってみると、Hello Worldが表示されました！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651150/f31d872c-b08f-84af-55e6-4b86e4da1b8a.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651150/8cf0db52-de1d-a88f-128a-d747653658d1.png)

無事、異なるオリジン間のFE<->BEのHTTP通信を疎通することができました。

アプリケーションの処理の前に、CORSのチェックをMiddlewareで行うことで、**そもそも意図しないオリジンとの通信を行わない**制御が可能になります。


# 終わりに
EchoのMiddlewareツールを使って開発することで、アプリケーションの可用性やセキュリティを向上させるようなMiddlewareを簡単に実装することができます。これらの機能を活用することで、より効果的なバックエンド開発が可能になります。

EchoのMiddlewareツールだと他にも、Basic AuthやSessionなど興味があるので、こちらも触ってみようとおもいます。
また、弊社では、Middlewareをさらにカスタマイズしていますので、カスタム内容についても深ぼっていきたいと思います。

以上でWanoグループのアドベントカレンダー2023を全て終了します！
今年も皆さんお疲れさまでした！
それでは、良いお年を！！

# 採用もやっています！
現在、Wanoグループ / TuneCore Japan では人材募集をしています。興味のある方は下記を参照してください。

Wano | [Wano Group JOBS](https://group.wano.co.jp/jobs/)
TuneCore Japan | [TuneCore Japan JOBS](https://www.tunecore.co.jp/jobs)

# 参考
https://medium.com/codex/what-the-hell-is-middleware-ebd1a7bdd181