LoginSignup
2
0

レスポンスボディに書き込んだ内容を置き換えるginのミドルウェアを作った

Posted at

レスポンスボディに書き込んだ内容を置き換えるミドルウェアを作った。

r := gin.Default()
r.Use(ginreplacer.New(&ginreplacer.Config{
  Replacer: strings.Replacer("old", "new"),
}))

r.GET("", func(ctx *gin.Context) {
  ctx.String(http.StatusOK, "old")
})

上記のようなコードを書いて /GETすると newと返ってくる。

$ curl localhost:8080
new

具体的な利用例

注意: 作ったばかりなので本格的には使えてない

exampleでも実装しているけど、フロントエンドの静的ファイルを返す際に、APIのbaseURLなどを動的に変えたい場合。

const apiBaseURL = "%APIBASEURL%";

document.getElementById("requestButton").addEventListener("click", async () => {
    const res = await fetch(apiBaseURL+"/ping")
    document.getElementById("response").innerText = (await res.json()).message;
})

package main

import (
	"embed"
	"net/http"
	"path/filepath"
	"strings"

	"github.com/gin-contrib/static"
	"github.com/gin-gonic/gin"
	ginreplacer "github.com/ophum/gin-replacer"
)

//go:embed root/*
var fs embed.FS

func main() {
	router := gin.Default()

	router.Use(ginreplacer.New(&ginreplacer.Config{
        // jsだけ置き換え処理を行う
		IgnoreFunc: func(ctx *gin.Context) bool {
			return filepath.Ext(ctx.Request.URL.Path) != ".js"
		},
        // index.jsにある %APIBASEURL%をhttp://localhost:8080/apiに置き換える
		Replacer: strings.NewReplacer(
			"%APIBASEURL%", "http://localhost:8080/api",
		),
	}))
    // gin-contrib/staticでembed.FSのファイル(index.html, index.js)を返す
	router.Use(static.Serve("", static.EmbedFolder(fs, "root")))

	router.GET("/api/ping", func(ctx *gin.Context) {
		ctx.JSON(http.StatusOK, gin.H{
			"message": "pong",
		})
	})

	if err := router.Run(); err != nil {
		panic(err)
	}
}

仕組み

仕組みは単純で、middlewareでctx.Writerを独自のWriterに置き換えている。

レスポンスボディに書き込む際、ctx.Writer.Write([]byte)に内容が渡される。
そこで、独自のWriterのWrite関数で内容を置き換え、置き換え後の内容を元のgin.ResponseWriter.Write([]byte)に渡すことで実現している。

type replacerWriter struct {
	gin.ResponseWriter
	replacer *strings.Replacer
}

func (w *replacerWriter) Write(b []byte) (int, error) {
	oldLen := len(b)
	b = []byte(w.replacer.Replace(string(b)))
	newLen := len(b)

	contentLength := w.ResponseWriter.Header().Get("Content-Length")
    // Content-Lengthがセットされている場合、その値以上書き込むとエラーになるため、差分を調整する。
	if contentLength != "" {
		if err := w.adjustContentLength(contentLength, oldLen, newLen); err != nil {
			return 0, err
		}
	}
    // 置き換え後の内容を本来のレスポンスボディに書き込む。
	return w.ResponseWriter.Write(b)
}

func (w *replacerWriter) adjustContentLength(currentContentLength string, oldLen, newLen int) error {
	contentLength, err := strconv.ParseInt(currentContentLength, 10, 64)
	if err != nil {
		return err
	}
	adjustContentLength := int(contentLength) + (newLen - oldLen)
	w.ResponseWriter.Header().Set("Content-Length", fmt.Sprint(adjustContentLength))
	return nil
}

func New(config *Config) gin.HandlerFunc {
	if config.IgnoreFunc == nil {
		config.IgnoreFunc = defaultIgnoreFunc
	}

	return func(ctx *gin.Context) {
		if !config.IgnoreFunc(ctx) {
            // ctx.WriterをreplacerWriterでラップし、ctx.Writerを置き換えることで、このミドルウェア以降の処理ではreplacerWriterのWrite関数を使って書き込まれる。
			w := &replacerWriter{
				ResponseWriter: ctx.Writer,
				replacer:       config.Replacer,
			}
			ctx.Writer = w
		}
		ctx.Next()
	}
}
2
0
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
2
0