LoginSignup
1
1

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

Last updated at Posted at 2024-03-15

Go で HTTP リクエストを見てみよう!

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

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

まずは復習。シンプルに hello! と返すサーバを立ててみる。

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.ResponseWriterfmt.Fprintf(w io.Writer, format string, a ...any) もどちらも io.Write() を実装しているので、fmt.Fprintf() で書ける。

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

Request 開始行

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

// 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 を見てみる

// 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 でエンコーディングするものはたくさんあったが意外にも生で受け取るものはなかったので苦労した。

// 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.ReadCloserio.Closer(=> io.Close()) と io.Reader(=>io.Read()) を満たすので、io.ReadAll() を使って全部を読み込むことができる。
(io.ReadAll()io.Reader を引数に取り、EOFまで読み込み []byte を返す。)

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

bash で POST してみると

$  curl -X POST -H "Content-Type: text/plain" -d "hoge " 127.0.0.1:8080/req-body
Body:
hoge

ちなみに Content-Lengthがわかれば

// 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 で送ってみると

$  curl -X POST -H "Content-Type: text/plain" -d "hoge " 127.0.0.1:8080/req-body-len
EOF

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

ファイル全体


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 でのテキストの受け取り方

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

と言われる。そもそも公式ドキュメントにもこう書いてある

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, (accessed: 2024-03-16)

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

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

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

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1