レスポンスボディに書き込んだ内容を置き換えるミドルウェアを作った。
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()
}
}