Go の net/http(とecho) で HTTP リクエストを見てみる

## Go で HTTP リクエストを見てみよう!
Go で HTTP リクエストを見てみる。    
HTTP リクエストとは、**クライアント => サーバ** に送られるもので、クライアントの情報が書いてある。サーバはこれを頼りに情報を送り返す。  


例
```HTTPResponse
GET /req-header HTTP/1.1        # メソッド URI HTTPバージョン
Accept-Encoding: gzip, deflate, br    # ヘッダ
Accept-Language: ja
Host: localhost:8080
                                # 空行
                                # あればボディ
```

まずは復習。シンプルに `hello!` と返すサーバを立ててみる。
```go
func main(){
    http.HandleFunc("/simple", simple)
    http.ListenAndServe(":8080", nil)
}

// w http.ResponseWriter, r *http.Request を満たしていればハンドラ関数になれる。
func simple(w http.ResponseWriter, r *http.Request) {

	// func fmt.Fprintf(w io.Writer, format string, a ...any) (n int, err error)
	// `io.Writer` は ファイルやネットワークのWiriteをIOのように抽象化したインタフェース(全てファイルととらえるLinuxとかと似たようなモノ。ファイルディスクリプタ)
	//  `Write(p []byte) (n int, err error)` を満たせばいい。
	fmt.Fprintf(w, "hello!")
}
```
`http.ResponseWriter` も `fmt.Fprintf(w io.Writer, format string, a ...any)` もどちらも `io.Write()` を実装しているので、`fmt.Fprintf()` で書ける。 

`io.Writer` は ファイルやネットワークのWiriteをIOのように抽象化したインタフェース(全てファイルととらえるLinuxとかと似たようなモノ。ファイルディスクリプタ)

### Request 開始行

では開始行 `GET /req-header HTTP/1.1` はどうやって得るのか

```go
// main()
// http.HandleFunc("/req-start-line", reqStartLine)

// HTTP Request 開始行を見ていく
func reqStartLine(w http.ResponseWriter, r *http.Request) {
	// HTTPメソッド
	fmt.Fprintln(w, "Method:", r.Method)    // GET 
	// URL
	fmt.Fprintln(w, "URL:", r.URL)          // /req-start-line
	// HTTPバージョン
	fmt.Fprintln(w, "Proto:", r.Proto)      // HTTP/1.1

	// いつも見るやつ
	fmt.Fprintln(w, r.Method, r.URL, r.Proto) // GET / HTTP/1.1
}
```

それぞれ `http.Request.Method`, `http.Request.URL`, `http.Request.Proto` で見ることができる。  

ブラウザにアクセスすると
```
# http://localhost:8080/req-start-line にアクセス

Method: GET
URL: /req-start-line
Proto: HTTP/1.1
GET /req-start-line HTTP/1.1
```

### Request Headers

Request Header を見てみる

```go
// main()
// http.HandleFunc("/req-header", reqHeader)

// HTTP Header を見ていく
func reqHeader(w http.ResponseWriter, r *http.Request) {
	// `http.Request.Header` は map なので range で出せる
	for k, v := range r.Header {
		fmt.Fprintln(w, k, ":\t", v)
	}
}
```

`http.Request.Header` で取得できるが、`map[string][]string` であるので、キー&値の形で `range` で取得できる。

ブラウザにアクセスすると
```
Sec-Fetch-Dest :	 [document]
Sec-Ch-Ua :	 ["Chromium";v="122", "Not(A:Brand";v="24", "Brave";v="122"]
Upgrade-Insecure-Requests :	 [1]
User-Agent :	 [Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36]
Accept :	 [text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8]
Accept-Language :	 [ja]
Sec-Ch-Ua-Platform :	 ["Windows"]
Connection :	 [keep-alive]
Sec-Ch-Ua-Mobile :	 [?0]
Sec-Fetch-User :	 [?1]
Accept-Encoding :	 [gzip, deflate, br]
Sec-Gpc :	 [1]
Sec-Fetch-Site :	 [none]
Sec-Fetch-Mode :	 [navigate]
```
長い

### Request Body
POST等で使う Body を見てみる。巷には JSON でエンコーディングするものはたくさんあったが意外にも生で受け取るものはなかったので苦労した。

```Go
// main()
// http.HandleFunc("/req-body", reqBody)

// HTTP Body を見る (POST 等で使うやつ)
func reqBody(w http.ResponseWriter, r *http.Request) {
	// `r.Body` は `io.ReadCloser` インタフェース型。
	// `io.ReadCloser` は `io.Closer`(=> `io.Close()`) と `io.Reader`(=>`io.Read()`) を満たす。
	// `io.ReadAll()` は `io.Reader` を引数に取り、EOFまで読み込み []byte を返す。
    // 昔は `ioutil.ReadAll()` だったらしいので注意 
	body, err := io.ReadAll(r.Body)
	if err != nil {
		fmt.Fprintln(w, err)
		return
	}
	fmt.Fprint(w, "Body:\n", string(body))
}
```

`http.Request.Body` でボディを取得できるが、 `io.ReadCloser` インタフェース型である。   
`io.ReadCloser` は `io.Closer`(=> `io.Close()`) と `io.Reader`(=>`io.Read()`) を満たすので、`io.ReadAll()` を使って全部を読み込むことができる。   
(`io.ReadAll()` は `io.Reader` を引数に取り、EOFまで読み込み []byte を返す。)

昔は `ioutil.ReadAll()` だったらしいので注意が必要である

bash で POST してみると
```sh
$  curl -X POST -H "Content-Type: text/plain" -d "hoge " 127.0.0.1:8080/req-body
Body:
hoge
```


ちなみに `Content-Length`がわかれば
```go
// Content-Length で長さを取得できるため、ReadAll を使わなくてもよくなる。
func reqBodyLen(w http.ResponseWriter, r *http.Request) {

	// r.ContentLength でデータ量取得
	length := r.ContentLength

	// スライス確保
	body := make([]byte, length)

	//　`r.Body` は `io.ReadCloser` なので `io.Reader` を満たし、 `io.Read()` の実装を持っている
	_, err := r.Body.Read(body)

	if err != nil {
		fmt.Fprintln(w, err)
		return
	}
	fmt.Fprint(w, "Body:\n", string(body))
}
```
このようなことができる。ただし、 `Content-Length` は設定されていない場合や偽造もできるのであまりよくないことは良くない。(送り主の良心次第)

先ほどと同じように Bash で送ってみると
```sh
$  curl -X POST -H "Content-Type: text/plain" -d "hoge " 127.0.0.1:8080/req-body-len
EOF

```

`Content-Length` を設定していないので、EOF err になってしまった。


### ファイル全体

```go

package main

import (
	"fmt"
	"io"
	"net/http"
)

func main() {

	http.HandleFunc("/simple", simple)
	http.HandleFunc("/req-start-line", reqStartLine)
	http.HandleFunc("/req-header", reqHeader)
	http.HandleFunc("/req-body", reqBody)
	http.HandleFunc("/req-body-len", reqBodyLen)

	http.ListenAndServe(":8080", nil)
}

// w http.ResponseWriter, r *http.Request を満たしていればハンドラ関数になれる。
func simple(w http.ResponseWriter, r *http.Request) {

	// func fmt.Fprintf(w io.Writer, format string, a ...any) (n int, err error)
	// `io.Writer` は ファイルやネットワークのWiriteをIOのように抽象化したインタフェース(全てファイルととらえるLinuxとかと似たようなモノ。ファイルディスクリプタ)
	//  `Write(p []byte) (n int, err error)` を満たせばいい。
	fmt.Fprintf(w, "hello!")
}

// HTTP Request 開始行を見ていく
func reqStartLine(w http.ResponseWriter, r *http.Request) {
	// HTTPメソッド
	fmt.Fprintln(w, "Method:", r.Method)
	// URL
	fmt.Fprintln(w, "URL:", r.URL)
	// HTTPバージョン
	fmt.Fprintln(w, "Proto:", r.Proto)

	// いつも見るやつ
	fmt.Fprintln(w, r.Method, r.URL, r.Proto) // GET / HTTP/1.1
}

// HTTP Header を見ていく
func reqHeader(w http.ResponseWriter, r *http.Request) {
	// `http.Request.Header` は map なので range で出せる
	for k, v := range r.Header {
		fmt.Fprintln(w, k, ":\t", v)
	}
}

// HTTP Body を見る (POST 等で使うやつ)
func reqBody(w http.ResponseWriter, r *http.Request) {
	// `r.Body` は `io.ReadCloser` インタフェース型。
	// `io.ReadCloser` は `io.Closer`(=> `io.Close()`) と `io.Reader`(=>`io.Read()`) を満たす。
	// `io.ReadAll()` は `io.Reader` を引数に取り、EOFまで読み込み []byte を返す。
	body, err := io.ReadAll(r.Body)
	if err != nil {
		fmt.Fprintln(w, err)
		return
	}
	fmt.Fprintln(w, "Body:\n", string(body))
}

// Content-Length で長さを取得できるため、ReadAll を使わなくてもよくなる。あんまり安心はできない。
func reqBodyLen(w http.ResponseWriter, r *http.Request) {

	// r.ContentLength でデータ量取得
	length := r.ContentLength

	// スライス確保
	body := make([]byte, length)

	//　`r.Body` は `io.ReadCloser` なので `io.Reader` を満たし、 `io.Read()` の実装を持っている
	_, err := r.Body.Read(body)

	if err != nil {
		fmt.Fprintln(w, err)
		return
	}
	fmt.Fprintln(w, "Body:\n", string(body))
}
```

### 余談(本命?): echo を使った POST でのテキストの受け取り方

```go
func main(){
    e := echo.New()
	e.POST("/post/txt", postTxt)
	e.Logger.Fatal(e.Start("localhost:1323"))
}


func postTxt(c echo.Context) error {
    // `echo.Context` は `*http.Request` を返す `Request()` 関数を持つ。 
    // なのでそのまま `echo.Context.Request().Body` とすれば上記と同じように 
    // `io.ReadAll()` で読み込める
	body, err := io.ReadAll(c.Request().Body)

    // `echo.Context.Bind()` は使えない。
    // body := new(string); err := c.Bind(body); // <= ダメ

	fmt.Println(err, body)
	if err != nil {
		return c.String(http.StatusOK, "bad request")
	} else {
		return c.String(http.StatusOK, "your request:\n"+string(body))
	}
}
```

echo パッケージ等で、POST でプレーンテキスト(`Content-Type: text/plain`)を受け取りたいときがある。   
でも調べても JSON の受け取り方法しか出てこないので困ったことがあった...

しかし、よく型を見てみると `echo.Context` インタフェースは `*http.Request` を返す `Request()` 関数を持っており、   
なのでそのまま`echo.Context.Request().Body` とすれば上記と同じように `Body` を得ることができて、`io.ReadAll(c.Request().Body)` で読み込むことができる。

また普通に `http.Request` と同じなので、ボディだけではなくヘッダ等も見ることができる(コード略)。

調べると JSON を `echo.Context.Bind()` でバインドし、構造体にするコードがよく出てくるが、 `Bind()` は `Content-Type` として `text/plain` は受け取らないらしい。
```
code=415, message=Unsupported Media Type
```

と言われる。そもそも[公式ドキュメント](https://echo.labstack.com/docs/binding)にもこう書いてある

> ### Data Types
> When decoding the request body, the following data types are supported as specified by the Content-Type header:
> - application/json
> - application/xml
> - application/x-www-form-urlencoded





### HTTP Request_ref

1. twihike's website. "リクエスト".    
    [https://www.twihike.dev/docs/golang-web/requests](https://www.twihike.dev/docs/golang-web/requests), (accessed: 2024-03-16)

1. @BitterBamboo. "Go における HTTP リクエストの受け取り方". Qiita.   
    [https://qiita.com/BitterBamboo/items/182659dddc5b4b195976](https://qiita.com/BitterBamboo/items/182659dddc5b4b195976), (accessed: 2024-03-16)

1. Go Packages. "io".    
    [https://pkg.go.dev/io](https://pkg.go.dev/io), (accessed: 2024-03-16)


1. Echo, LabStack LLC. "Binding". Echo.
    [https://echo.labstack.com/docs/binding](https://echo.labstack.com/docs/binding), (accessed: 2024-03-17)