質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.80%

GoでPOSTリクエストのデータを取得する方法

解決済

回答 1

投稿

  • 評価
  • クリップ 0
  • VIEW 623

d_tutuz

score 545

Go で POST リクエストされたデータを Read する方法がわからないです。

*http.Request の ParseForm() メソッドを用いることが一般的ですが、以下のような net/http パッケージを使用しない場合で、適切な扱い方がわからないです。

教えていただきたいこと

net/http パッケージを使用せず、POST リクエストのデータを取得する方法を教えていただきたいです。

実装

https://github.com/GoesToEleven/GolangTraining/blob/master/27_code-in-process/42_HTTP/02_http-server/i05_not-writing_error-in-code/main.go

以下のコードが例です。上記の URL に記載されている URL と同様です。

package main

import (
    "bufio"
    "fmt"
    "io"
    "log"
    "net"
    "strconv"
    "strings"
)

func handleConn(conn net.Conn) {
    defer conn.Close()
    scanner := bufio.NewScanner(conn)
    i := 0
    headers := map[string]string{}
    var url, method string
    for scanner.Scan() {
        ln := scanner.Text()
        fmt.Println(ln)

        if i == 0 {
            fs := strings.Fields(ln)
            method := fs[0]
            url = fs[1]
            fmt.Println("METHOD", method)
            fmt.Println("URL", url)
        } else {
            // in headers now
            // when line is empty, header is done
            if ln == "" {
                break
            }
            fs := strings.SplitN(ln, ": ", 2)
            headers[fs[0]] = fs[1]
        }

        i++
    }

    // parse body

    // ここがポイントと思われる
    if method == "POST" || method == "PUT" {
        amt, _ := strconv.Atoi(headers["Content-Length"])
        buf := make([]byte, amt)
        // 単に ReadFull ではデータを読み込むことができない
        io.ReadFull(conn, buf)
        // in buf we will have the POST content
        fmt.Println("BODY:", string(buf))
    }
    // ここまで

    // response
    body := `
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    <form method="POST">
        <input type="text" name="key" value="">
        <input type="submit">
    </form>
</body>
</html>
    `

    io.WriteString(conn, "HTTP/1.1 200 OK\r\n")
    fmt.Fprintf(conn, "Content-Length: %d\r\n", len(body))
    io.WriteString(conn, "\r\n")
    io.WriteString(conn, body)
}

func main() {
    server, err := net.Listen("tcp", ":9000")
    if err != nil {
        log.Fatalln(err.Error())
    }
    defer server.Close()

    for {
        conn, err := server.Accept()
        if err != nil {
            log.Fatalln(err.Error())
        }
        go handleConn(conn)
    }
}
  • クライアントの POST リクエスト

このときクライアントの curl で以下のように POST リクエストを送信したとします。

curl -v http://localhost:9000 -X POST -d "Sample message."
  • サーバに出力されるメッセージ

しかしながらサーバ側には以下のように POST リクエストで送信されているデータを読み込むことができません。

POST / HTTP/1.1
METHOD POST
URL /
Host: localhost:9000
User-Agent: curl/7.55.1
Accept: */*
Content-Length: 4
Content-Type: application/x-www-form-urlencoded

"BODY:": Sample message. が表示されることを想定しているのですが、POST リクエストのデータが読み込めない理由がわからず、教えていただけると助かります。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+2

なぜ標準ライブラリを使わずに書いているのかは学習目的と仮定します。

まずは、「io.ReadFull」の戻り値のerror値の内容を確認するように修正してください。もしnilではないのであればその内容を追記してください。
「io.ReadFull」の次の行が実行されない可能性もあります。

io.ReadFullの次に処理が進むのかを確認し、
戻り値のerrorをログに出すなりしてその内容を教えて下さい。

「io.ReadFull」の次の行が実行されない可能性の原因として考えられるのは
HTTP1.1以降はConnectionヘッダを省略したリクエストはコネクションをキープしようとするという挙動です。
HTTP1.1の接続仕様(MDN)

curlの「-H `Connection: close'」などを付け足して見てみるのも良いかもしれません。

追記

動くようなコードに修正してみました。 net/textprotoのReaderの場合、必要以上に読むことはないのでこういう用途に適しています。

package main

import (
    "bufio"
    "fmt"
    "io"
    "log"
    "net"
    "net/textproto"
    "strconv"
    "strings"
)

func handleConn(conn net.Conn) {
    defer conn.Close()
    reader := bufio.NewReader(conn)
    scanner := textproto.NewReader(reader)
    i := 0
    headers := map[string]string{}
    var url, method string
    for {
        ln, err := scanner.ReadLine()
        if err != nil {
            log.Println(err)
            return
        }

        if i == 0 {
            fs := strings.Fields(ln)
            method = fs[0]
            url = fs[1]
            fmt.Println("METHOD", method)
            fmt.Println("URL", url)
        } else {
            // in headers now
            // when line is empty, header is done
            if ln == "" {
                break
            }
            fs := strings.SplitN(ln, ": ", 2)
            headers[fs[0]] = fs[1]
        }

        i++
    }

    // parse body

    // ここがポイントと思われる
    if method == "POST" || method == "PUT" {
        amt, _ := strconv.Atoi(headers["Content-Length"])
        buf := make([]byte, amt)
        // 単に ReadFull ではデータを読み込むことができない
        _, err := io.ReadFull(reader, buf)
        if err != nil {
            fmt.Println("error:", err)
        }
        // in buf we will have the POST content
        fmt.Println("BODY:", string(buf))
    }
    // ここまで
    // response
    body := `
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    <form method="POST">
        <input type="text" name="key" value="">
        <input type="submit">
    </form>
</body>
</html>
    `

    io.WriteString(conn, "HTTP/1.1 200 OK\r\n")
    fmt.Fprintf(conn, "Content-Length: %d\r\n", len(body))
    io.WriteString(conn, "\r\n")
    io.WriteString(conn, body)
}

func main() {
    server, err := net.Listen("tcp", ":9000")
    if err != nil {
        log.Fatalln(err.Error())
    }
    defer server.Close()

    for {
        conn, err := server.Accept()
        if err != nil {
            log.Fatalln(err.Error())
        }
        go handleConn(conn)
    }
}

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/11/22 22:43 編集

    nobonobo さん
    > method := fs[0]にて「var method」とは別の小さなスコープにmethodを再定義してしまっています。なので外側のmethodには値は空文字のままです。
    そうですね、確認不足でした。`method = fs[0]` などとしてクロージャとして参照しないといけないですね。。。

    > おそらく、一度Scanを始めてしまったScannerは別の方法でバッファ内容にアクセスする方法はありません。ヘッダーの読み込みにはもう少し低レベルな方法で読み出す必要があるでしょう。
    こちらありがとうございます。ヘッダーの読み込みに別の方法を検討してみます。

    キャンセル

  • 2019/11/22 22:54

    標準のnet/httpライブラリはnet/textprotoのReaderを使ってヘッダーをパースしています。net/textprotoを使った例を追記しました。

    キャンセル

  • 2019/11/22 22:55

    nobonobo さん

    net/textproto パッケージ勉強になりました。感謝です、ありがとうございます!!

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.80%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る