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.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
はどうやって得るのか
// 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.ReadCloser
は io.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
-
twihike's website. "リクエスト".
https://www.twihike.dev/docs/golang-web/requests, (accessed: 2024-03-16) -
@BitterBamboo. "Go における HTTP リクエストの受け取り方". Qiita.
https://qiita.com/BitterBamboo/items/182659dddc5b4b195976, (accessed: 2024-03-16) -
Go Packages. "io".
https://pkg.go.dev/io, (accessed: 2024-03-16) -
Echo, LabStack LLC. "Binding". Echo.
https://echo.labstack.com/docs/binding, (accessed: 2024-03-17)