Goで外部通信の中身を確認する

使用しているライブラリの外部通信の中身(リクエストのヘッダやレスポンスのステータス等)をデバッグのために出力したくなったのでまとめました。

方法

以下のように外部のサーバーにリクエストするようなコードがあったとします。

package main

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

func main() {
	if err := run(); err != nil {
		panic(err)
	}
}

var c = &http.Client{}

func run() error {
	ctx := context.Background()
	req, err := http.NewRequestWithContext(
		ctx,
		http.MethodGet,
		"http://localhost:8080",
		nil,
	)
	if err != nil {
		return err
	}
	res, err := c.Do(req)
	if err != nil {
		return err
	}
	fmt.Println(res.Status)
	return nil
}

この時、外部との通信の中身を確認したい場合、以下のようなコードを追加すると出力できます。

func init() {
	http.DefaultTransport = &myRoundTripper{
		RoundTripper: http.DefaultTransport,
	}
}

type myRoundTripper struct {
	http.RoundTripper
}

func (r *myRoundTripper) RoundTrip(req *http.Request) (
	*http.Response, error) {
	fmt.Println("req.Header:", req.Header)

	res, err := r.RoundTripper.RoundTrip(req)

	fmt.Println("res.Status:", res.Status)
	return res, err
}
$ go run main.go
req.Header: map[]
res.Status: 200 OK

コードのどこでも良いので、 init()http.DefaultTransport を上書きすればOKです。

解説

http.Client はリクエストを送信する時、内部で Client.Transport を使用しますが、設定されていなければ http.DefaultTransport を使うよになっています。

https://cs.opensource.google/go/go/+/master:src/net/http/client.go;l=170

func (c *Client) send(req *Request, deadline time.Time) (
	resp *Response, didTimeout func() bool, err error) {
	...
	resp, didTimeout, err = send(req, c.transport(), deadline)
	...
}

...

func (c *Client) transport() RoundTripper {
	if c.Transport != nil {
		return c.Transport
	}
	return DefaultTransport
}

この RoundTripper は以下の定義のinterfaceです。

https://cs.opensource.google/go/go/+/master:src/net/http/client.go;l=117

type RoundTripper interface {
	RoundTrip(*Request) (*Response, error)
}

そのため、このinterfaceを満たす構造体を定義し、http.DefaultTransport を上書きすると、通信の前後に任意の処理を挟むことができます。

func init() {
	http.DefaultTransport = &myRoundTripper{
		RoundTripper: http.DefaultTransport,
	}
}

type myRoundTripper struct {
	http.RoundTripper
}

func (r *myRoundTripper) RoundTrip(req *http.Request) (
	*http.Response, error) {

	// reqを出力

	// 実際の通信は元々の http.DefaultTransport に任せる
	res, err := r.RoundTripper.RoundTrip(req)

	// resを出力

	return res, err
}

この処理は http.DefaultTransport を使う全ての通信に適用されます。例えば、 github.com/aws/aws-sdk-goデフォルトで http.DefaultClient を使うので、以下のコードで通信を出力できます。

package main

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

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/dynamodb"
)

func init() {
	http.DefaultTransport = &myRoundTripper{
		RoundTripper: http.DefaultTransport,
	}
}

type myRoundTripper struct {
	http.RoundTripper
}

func (r *myRoundTripper) RoundTrip(req *http.Request) (
	*http.Response, error) {
	fmt.Println("req.URL:", req.URL.String())
	if req.Body != nil {
		reqBody, _ := io.ReadAll(req.Body)
		req.Body.Close()
		req.Body = io.NopCloser(bytes.NewBuffer(reqBody))
		fmt.Println("req.Body:", string(reqBody))
	}

	res, err := r.RoundTripper.RoundTrip(req)

	fmt.Println("res.Status:", res.Status)
	if res.Body != nil {
		resBody, _ := io.ReadAll(res.Body)
		res.Body.Close()
		res.Body = io.NopCloser(bytes.NewBuffer(resBody))
		fmt.Println("res.Body:", string(resBody))
	}
	return res, err
}

func main() {
	if err := run(); err != nil {
		panic(err)
	}
}

func run() error {
	sess := session.Must(session.NewSession(&aws.Config{
		Region: aws.String("ap-northeast-1"),
	}))
	client := dynamodb.New(sess)
	res, err := client.ListTables(&dynamodb.ListTablesInput{})
	if err != nil {
		return err
	}
	fmt.Println(res)

	return nil
}
$ go run main.go
req.URL: https://dynamodb.ap-northeast-1.amazonaws.com/
req.Body: {}
res.Status: 200 OK
res.Body: {"TableNames":...}

ちなみに github.com/aws/aws-sdk-go-v2*http.Transport を自前で生成しているので、同じようにしてもログは出ません。

func run() error {
	ctx := context.Background()
	cfg, err := config.LoadDefaultConfig(
		ctx,
		config.WithRegion("ap-northeast-1"),
	)
	if err != nil {
		return err
	}
	client := v2dynamodb.NewFromConfig(cfg)
	res, err := client.ListTables(ctx, &v2dynamodb.ListTablesInput{})
	if err != nil {
		return err
	}
	fmt.Println(res)

	return nil
}

使用する http.Client を指定するとログが出るようになります。

func run() error {
	ctx := context.Background()
	cfg, err := config.LoadDefaultConfig(
		ctx,
		config.WithRegion("ap-northeast-1"),
		// http.DefaultClient を設定
		config.WithHTTPClient(http.DefaultClient),
	)
	...
$ go run main.go
req.URL: https://dynamodb.ap-northeast-1.amazonaws.com/
req.Body: {}
res.Status: 200 OK
res.Body: {"TableNames":...}

大抵のライブラリはこのようにクライアントを設定する方法があると思うので、同じように出力できます。