Repository: gin-gonic/gin
Pull request #4197 (2025-03-25 00:07:49)

chore(deps): bump github.com/quic-go/quic-go from 0.48.2 to 0.50.1
Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.48.2 to 0.50.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/quic-go/quic-go/releases">github.com/quic-go/quic-go's releases</a>.</em></p>
<blockquote>
<h2>v0.50.1</h2>
<p>This patch release contains a backported fix for a remote-triggered panic in the probe packet loss detection logic: <a href="https://redirect.github.com/quic-go/quic-go/issues/4998">#4998</a>.</p>
<p><strong>Full Changelog</strong>: <a href="https://github.com/quic-go/quic-go/compare/v0.50.0...v0.50.1">https://github.com/quic-go/quic-go/compare/v0.50.0...v0.50.1</a></p>
<h2>v0.50.0</h2>
<p>This release implements server-side path-probing (as described in section 9 of RFC 9000): <a href="https://redirect.github.com/quic-go/quic-go/issues/4932">#4932</a>, <a href="https://redirect.github.com/quic-go/quic-go/issues/4933">#4933</a>, <a href="https://redirect.github.com/quic-go/quic-go/issues/4935">#4935</a>, <a href="https://redirect.github.com/quic-go/quic-go/issues/4938">#4938</a>, <a href="https://redirect.github.com/quic-go/quic-go/issues/4939">#4939</a>, <a href="https://redirect.github.com/quic-go/quic-go/issues/4940">#4940</a>, <a href="https://redirect.github.com/quic-go/quic-go/issues/4941">#4941</a>, <a href="https://redirect.github.com/quic-go/quic-go/issues/4944">#4944</a>, <a href="https://redirect.github.com/quic-go/quic-go/issues/4947">#4947</a>, <a href="https://redirect.github.com/quic-go/quic-go/issues/4959">#4959</a>.</p>
<p>When the server receives a packet for an existing connection from a different IP address / port, it first needs to probe the new path before it can send packets on that path. This happens when the client experiences a NAT rebinding, and when the client attempts to migrate to a new connection. Previous versions of quic-go would accept the packets from the new path, but never switch to the new path.</p>
<p>Note that the client side connection migration logic (<a href="https://redirect.github.com/quic-go/quic-go/issues/234">#234</a>) is not yet implemented in quic-go (but we're working on it!).</p>
<h2>Major Changes</h2>
<ul>
<li>use the new <a href="https://redirect.github.com/golang/go/issues/63691">crypto/tls 0-RTT API</a> that we helped design in 2023: <a href="https://redirect.github.com/quic-go/quic-go/issues/4953">#4953</a></li>
<li>use a ringbuffer to store received packets, significantly reducing memory consumption: <a href="https://redirect.github.com/quic-go/quic-go/issues/4929">#4929</a></li>
<li>according to our Go version policy, we removed support for Go 1.22. quic-go now requires Go 1.23 or Go 1.24: <a href="https://redirect.github.com/quic-go/quic-go/issues/4880">#4880</a></li>
<li>the connection timer logic was refactored, enabling future changes to this code path: <a href="https://redirect.github.com/quic-go/quic-go/issues/4927">#4927</a></li>
</ul>
<h2>Other Fixes</h2>
<ul>
<li>fix busy-looping when pacing packets and the send queue blocks: <a href="https://redirect.github.com/quic-go/quic-go/issues/4943">#4943</a></li>
<li>don't drop undecryptable packets when deriving 2 sets of keys at the same time (i.e. when resuming a 0-RTT connection): <a href="https://redirect.github.com/quic-go/quic-go/issues/4950">#4950</a></li>
</ul>
<h2>Go 1.24 FIPS 140-3 Caveats</h2>
<p>Go 1.24 made several changes related to FIPS 140-3 compliance. Among others, it introduced a <code>fips-only</code> mode (enabled by setting <code>GODEBUG=&quot;fips140=only&quot;</code>).</p>
<p>It is not possible to use quic-go in fips-only mode, since the QUIC RFC requires initializing an AES GCM cipher with a fixed nonce, which is considered unsafe according to FIPS 140-3, or at least the Go team's interpretation thereof. See <a href="https://redirect.github.com/quic-go/quic-go/issues/4894">quic-go/quic-go#4894</a> and the discussion on [Go issue <a href="https://redirect.github.com/quic-go/quic-go/issues/69536">#69536</a>](<a href="https://redirect.github.com/golang/go/issues/69536#issuecomment-2603807242">golang/go#69536</a>).</p>
<p>Before v0.50.0, quic-go would initialize the AES cipher on <code>init</code>, leading to a panic when using fips-only mode. For v0.50.0 we changed this behavior to lazy initialization (<a href="https://redirect.github.com/quic-go/quic-go/pull/4916">quic-go/quic-go#4916</a>). Note that this still means it's not possible to use QUIC in fips-only mode.</p>
<h2>Changelog</h2>
<ul>
<li>ackhandler: remove unneeded error return from packet history iterator by <a href="https://github.com/marten-seemann"><code>@​marten-seemann</code></a> in <a href="https://redirect.github.com/quic-go/quic-go/pull/4917">quic-go/quic-go#4917</a></li>
<li>proxy: remove Proxy.LocalPort method by <a href="https://github.com/marten-seemann"><code>@​marten-seemann</code></a> in <a href="https://redirect.github.com/quic-go/quic-go/pull/4920">quic-go/quic-go#4920</a></li>
<li>proxy: rename to Proxy, refactor initialization by <a href="https://github.com/marten-seemann"><code>@​marten-seemann</code></a> in <a href="https://redirect.github.com/quic-go/quic-go/pull/4921">quic-go/quic-go#4921</a></li>
<li>proxy: add function to simulate NAT rebinding by <a href="https://github.com/marten-seemann"><code>@​marten-seemann</code></a> in <a href="https://redirect.github.com/quic-go/quic-go/pull/4922">quic-go/quic-go#4922</a></li>
<li>proxy: optimize packet sorting logic by <a href="https://github.com/marten-seemann"><code>@​marten-seemann</code></a> in <a href="https://redirect.github.com/quic-go/quic-go/pull/4923">quic-go/quic-go#4923</a></li>
<li>simplify handling of packet unpacking errors by <a href="https://github.com/marten-seemann"><code>@​marten-seemann</code></a> in <a href="https://redirect.github.com/quic-go/quic-go/pull/4924">quic-go/quic-go#4924</a></li>
<li>refactor connection error propagation by <a href="https://github.com/marten-seemann"><code>@​marten-seemann</code></a> in <a href="https://redirect.github.com/quic-go/quic-go/pull/4925">quic-go/quic-go#4925</a></li>
<li>refactor packet handling into a separate function by <a href="https://github.com/marten-seemann"><code>@​marten-seemann</code></a> in <a href="https://redirect.github.com/quic-go/quic-go/pull/4926">quic-go/quic-go#4926</a></li>
<li>congestion: migrate the pacer tests away from Ginkgo by <a href="https://github.com/marten-seemann"><code>@​marten-seemann</code></a> in <a href="https://redirect.github.com/quic-go/quic-go/pull/4929">quic-go/quic-go#4929</a></li>
<li>utils: add a method to reset the RTTStats for connection migration by <a href="https://github.com/marten-seemann"><code>@​marten-seemann</code></a> in <a href="https://redirect.github.com/quic-go/quic-go/pull/4930">quic-go/quic-go#4930</a></li>
<li>add a packet packer method to pack path probe packets by <a href="https://github.com/marten-seemann"><code>@​marten-seemann</code></a> in <a href="https://redirect.github.com/quic-go/quic-go/pull/4932">quic-go/quic-go#4932</a></li>
<li>add a method to change the remote address of the sendConn by <a href="https://github.com/marten-seemann"><code>@​marten-seemann</code></a> in <a href="https://redirect.github.com/quic-go/quic-go/pull/4933">quic-go/quic-go#4933</a></li>
<li>ackhandler: add path probe tracking logic to sent packet history by <a href="https://github.com/marten-seemann"><code>@​marten-seemann</code></a> in <a href="https://redirect.github.com/quic-go/quic-go/pull/4934">quic-go/quic-go#4934</a></li>
<li>implement connection ID handling for path probe packets by <a href="https://github.com/marten-seemann"><code>@​marten-seemann</code></a> in <a href="https://redirect.github.com/quic-go/quic-go/pull/4935">quic-go/quic-go#4935</a></li>
<li>fix flaky TestConnectionReceivePrioritization by <a href="https://github.com/marten-seemann"><code>@​marten-seemann</code></a> in <a href="https://redirect.github.com/quic-go/quic-go/pull/4936">quic-go/quic-go#4936</a></li>
<li>add a method to the sendQueue to send probe packets by <a href="https://github.com/marten-seemann"><code>@​marten-seemann</code></a> in <a href="https://redirect.github.com/quic-go/quic-go/pull/4939">quic-go/quic-go#4939</a></li>
<li>implement a path manager to track the validation status of new paths by <a href="https://github.com/marten-seemann"><code>@​marten-seemann</code></a> in <a href="https://redirect.github.com/quic-go/quic-go/pull/4938">quic-go/quic-go#4938</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/quic-go/quic-go/commit/b90058aba5f65f48e0e150c89bbaa21a72dda4de"><code>b90058a</code></a> ackhandler: fix panic in probe packet tracking logic (<a href="https://redirect.github.com/quic-go/quic-go/issues/4998">#4998</a>)</li>
<li><a href="https://github.com/quic-go/quic-go/commit/d726a795db8391d29b180f684f74b223d40471a1"><code>d726a79</code></a> remove unneeded tracking of acknowledgments for PATH_CHALLENGEs (<a href="https://redirect.github.com/quic-go/quic-go/issues/4959">#4959</a>)</li>
<li><a href="https://github.com/quic-go/quic-go/commit/9f704c72cf6cb9a16d3726dcde2fd1046ee98e28"><code>9f704c7</code></a> ackhandler: fix handling of lost path probes on loss timer (<a href="https://redirect.github.com/quic-go/quic-go/issues/4956">#4956</a>)</li>
<li><a href="https://github.com/quic-go/quic-go/commit/bf28da834668dc6facfa972342e8cd383adea7a2"><code>bf28da8</code></a> handshake: use new crypto/tls 0-RTT API (<a href="https://redirect.github.com/quic-go/quic-go/issues/4953">#4953</a>)</li>
<li><a href="https://github.com/quic-go/quic-go/commit/b32f1fa0e41072effd47d523a60d3bb2fee68223"><code>b32f1fa</code></a> ackhandler: use Go iterators to iterate over sent packets (<a href="https://redirect.github.com/quic-go/quic-go/issues/4952">#4952</a>)</li>
<li><a href="https://github.com/quic-go/quic-go/commit/12f2be058b733ea90bb491a95c632a8f1742e23b"><code>12f2be0</code></a> bump go.mod version to Go 1.23, run 1.23 and 1.24 on CI (<a href="https://redirect.github.com/quic-go/quic-go/issues/4880">#4880</a>)</li>
<li><a href="https://github.com/quic-go/quic-go/commit/5af39164b9fefbe78eeac41e4ba0a59e2316cfbf"><code>5af3916</code></a> ci: update golangci-lint to v1.64.4 (<a href="https://redirect.github.com/quic-go/quic-go/issues/4951">#4951</a>)</li>
<li><a href="https://github.com/quic-go/quic-go/commit/48b81823394ae885bca954a2aaa39c0e746ee39a"><code>48b8182</code></a> keep undecryptable packets when deriving 0-RTT and handshake keys (<a href="https://redirect.github.com/quic-go/quic-go/issues/4950">#4950</a>)</li>
<li><a href="https://github.com/quic-go/quic-go/commit/5a1a34df3775a8c622a123f552d1acc6c8cdd07d"><code>5a1a34d</code></a> implement server-side path validation logic (<a href="https://redirect.github.com/quic-go/quic-go/issues/4944">#4944</a>)</li>
<li><a href="https://github.com/quic-go/quic-go/commit/ca26e98781a0d01105e115b1c3c15923a5fcc1da"><code>ca26e98</code></a> migrate the connection ID generator tests away from Ginkgo (<a href="https://redirect.github.com/quic-go/quic-go/issues/4948">#4948</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/quic-go/quic-go/compare/v0.48.2...v0.50.1">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/quic-go/quic-go&package-manager=go_modules&previous-version=0.48.2&new-version=0.50.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/docs/doc.md b/docs/doc.md
index cd65139..ce46665 100644
--- a/docs/doc.md
+++ b/docs/doc.md
@@ -1393,13 +1393,19 @@ func main() {
 
 ### HTML rendering
 
-Using LoadHTMLGlob() or LoadHTMLFiles()
+Using LoadHTMLGlob() or LoadHTMLFiles() or LoadHTMLFS()
 
 ```go
+//go:embed templates/*
+var templates embed.FS
+
 func main() {
   router := gin.Default()
   router.LoadHTMLGlob("templates/*")
   //router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
+  //router.LoadHTMLFS(http.Dir("templates"), "template1.html", "template2.html")
+  //or
+  //router.LoadHTMLFS(http.FS(templates), "templates/template1.html", "templates/template2.html")
   router.GET("/index", func(c *gin.Context) {
     c.HTML(http.StatusOK, "index.tmpl", gin.H{
       "title": "Main website",
diff --git a/gin.go b/gin.go
index 0761c14..f9813e1 100644
--- a/gin.go
+++ b/gin.go
@@ -16,6 +16,7 @@ import (
 	"sync"
 
 	"github.com/gin-gonic/gin/internal/bytesconv"
+	filesystem "github.com/gin-gonic/gin/internal/fs"
 	"github.com/gin-gonic/gin/render"
 
 	"github.com/quic-go/quic-go/http3"
@@ -285,6 +286,19 @@ func (engine *Engine) LoadHTMLFiles(files ...string) {
 	engine.SetHTMLTemplate(templ)
 }
 
+// LoadHTMLFS loads an http.FileSystem and a slice of patterns
+// and associates the result with HTML renderer.
+func (engine *Engine) LoadHTMLFS(fs http.FileSystem, patterns ...string) {
+	if IsDebugging() {
+		engine.HTMLRender = render.HTMLDebug{FileSystem: fs, Patterns: patterns, FuncMap: engine.FuncMap, Delims: engine.delims}
+		return
+	}
+
+	templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseFS(
+		filesystem.FileSystem{FileSystem: fs}, patterns...))
+	engine.SetHTMLTemplate(templ)
+}
+
 // SetHTMLTemplate associate a template with HTML renderer.
 func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
 	if len(engine.trees) > 0 {
diff --git a/ginS/gins.go b/ginS/gins.go
index 1dcb191..3e6a92e 100644
--- a/ginS/gins.go
+++ b/ginS/gins.go
@@ -32,6 +32,11 @@ func LoadHTMLFiles(files ...string) {
 	engine().LoadHTMLFiles(files...)
 }
 
+// LoadHTMLFS is a wrapper for Engine.LoadHTMLFS.
+func LoadHTMLFS(fs http.FileSystem, patterns ...string) {
+	engine().LoadHTMLFS(fs, patterns...)
+}
+
 // SetHTMLTemplate is a wrapper for Engine.SetHTMLTemplate.
 func SetHTMLTemplate(templ *template.Template) {
 	engine().SetHTMLTemplate(templ)
diff --git a/go.mod b/go.mod
index b993ca5..3a7e1ba 100644
--- a/go.mod
+++ b/go.mod
@@ -5,12 +5,12 @@ go 1.23.0
 require (
 	github.com/bytedance/sonic v1.13.1
 	github.com/gin-contrib/sse v1.1.0
-	github.com/go-playground/validator/v10 v10.22.1
+	github.com/go-playground/validator/v10 v10.26.0
 	github.com/goccy/go-json v0.10.2
 	github.com/json-iterator/go v1.1.12
 	github.com/mattn/go-isatty v0.0.20
 	github.com/pelletier/go-toml/v2 v2.2.2
-	github.com/quic-go/quic-go v0.48.2
+	github.com/quic-go/quic-go v0.51.0
 	github.com/stretchr/testify v1.10.0
 	github.com/ugorji/go/codec v1.2.12
 	golang.org/x/net v0.38.0
@@ -22,7 +22,7 @@ require (
 	github.com/bytedance/sonic/loader v0.2.4 // indirect
 	github.com/cloudwego/base64x v0.1.5 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/gabriel-vasile/mimetype v1.4.3 // indirect
+	github.com/gabriel-vasile/mimetype v1.4.8 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
 	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
@@ -35,12 +35,12 @@ require (
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/quic-go/qpack v0.5.1 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
-	go.uber.org/mock v0.4.0 // indirect
+	go.uber.org/mock v0.5.0 // indirect
 	golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
 	golang.org/x/crypto v0.36.0 // indirect
-	golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
-	golang.org/x/mod v0.17.0 // indirect
+	golang.org/x/mod v0.18.0 // indirect
+	golang.org/x/sync v0.12.0 // indirect
 	golang.org/x/sys v0.31.0 // indirect
 	golang.org/x/text v0.23.0 // indirect
-	golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
+	golang.org/x/tools v0.22.0 // indirect
 )
diff --git a/go.sum b/go.sum
index c107dc0..5a7f2ad 100644
--- a/go.sum
+++ b/go.sum
@@ -12,8 +12,8 @@ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQ
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
-github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
+github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
+github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
 github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
 github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
 github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
@@ -24,8 +24,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
 github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
-github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
+github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
 github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
 github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
 github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
@@ -61,8 +61,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
 github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
-github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
-github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
+github.com/quic-go/quic-go v0.51.0 h1:K8exxe9zXxeRKxaXxi/GpUqYiTrtdiWP8bo1KFya6Wc=
+github.com/quic-go/quic-go v0.51.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -81,16 +81,14 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
 github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
 github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
-go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
-go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
+go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
+go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
 golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
 golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
 golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
 golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
-golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
-golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
-golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
-golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
+golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
 golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
 golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
@@ -101,10 +99,8 @@ golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
 golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
 golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
-golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
-golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
+golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
 google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
 google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
diff --git a/internal/fs/fs.go b/internal/fs/fs.go
new file mode 100644
index 0000000..524ac08
--- /dev/null
+++ b/internal/fs/fs.go
@@ -0,0 +1,22 @@
+package fs
+
+import (
+	"io/fs"
+	"net/http"
+)
+
+// FileSystem implements an [fs.FS].
+type FileSystem struct {
+	http.FileSystem
+}
+
+// Open passes `Open` to the upstream implementation and return an [fs.File].
+func (o FileSystem) Open(name string) (fs.File, error) {
+	f, err := o.FileSystem.Open(name)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return fs.File(f), nil
+}
diff --git a/render/html.go b/render/html.go
index c308408..f5e7455 100644
--- a/render/html.go
+++ b/render/html.go
@@ -7,6 +7,8 @@ package render
 import (
 	"html/template"
 	"net/http"
+
+	"github.com/gin-gonic/gin/internal/fs"
 )
 
 // Delims represents a set of Left and Right delimiters for HTML template rendering.
@@ -31,10 +33,12 @@ type HTMLProduction struct {
 
 // HTMLDebug contains template delims and pattern and function with file list.
 type HTMLDebug struct {
-	Files   []string
-	Glob    string
-	Delims  Delims
-	FuncMap template.FuncMap
+	Files      []string
+	Glob       string
+	FileSystem http.FileSystem
+	Patterns   []string
+	Delims     Delims
+	FuncMap    template.FuncMap
 }
 
 // HTML contains template reference and its name with given interface object.
@@ -73,7 +77,11 @@ func (r HTMLDebug) loadTemplate() *template.Template {
 	if r.Glob != "" {
 		return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseGlob(r.Glob))
 	}
-	panic("the HTML debug render was created without files or glob pattern")
+	if r.FileSystem != nil && len(r.Patterns) > 0 {
+		return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseFS(
+			fs.FileSystem{FileSystem: r.FileSystem}, r.Patterns...))
+	}
+	panic("the HTML debug render was created without files or glob pattern or file system with patterns")
 }
 
 // Render (HTML) executes template and writes its result with custom ContentType for response.
diff --git a/render/json.go b/render/json.go
index fc8dea4..a6b54dc 100644
--- a/render/json.go
+++ b/render/json.go
@@ -151,7 +151,7 @@ func (r JsonpJSON) WriteContentType(w http.ResponseWriter) {
 }
 
 // Render (AsciiJSON) marshals the given interface object and writes it with custom ContentType.
-func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
+func (r AsciiJSON) Render(w http.ResponseWriter) error {
 	r.WriteContentType(w)
 	ret, err := json.Marshal(r.Data)
 	if err != nil {
@@ -159,12 +159,15 @@ func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
 	}
 
 	var buffer bytes.Buffer
+	escapeBuf := make([]byte, 0, 6) // Preallocate 6 bytes for Unicode escape sequences
+
 	for _, r := range bytesconv.BytesToString(ret) {
-		cvt := string(r)
 		if r >= 128 {
-			cvt = fmt.Sprintf("\\u%04x", int64(r))
+			escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x", r) // Reuse escapeBuf
+			buffer.Write(escapeBuf)
+		} else {
+			buffer.WriteByte(byte(r))
 		}
-		buffer.WriteString(cvt)
 	}
 
 	_, err = w.Write(buffer.Bytes())


Test patch:
diff --git a/gin_test.go b/gin_test.go
index 850ae09..a80b690 100644
--- a/gin_test.go
+++ b/gin_test.go
@@ -325,6 +325,115 @@ func TestLoadHTMLFilesFuncMap(t *testing.T) {
 	assert.Equal(t, "Date: 2017/07/01", string(resp))
 }
 
+var tmplFS = http.Dir("testdata/template")
+
+func TestLoadHTMLFSTestMode(t *testing.T) {
+	ts := setupHTMLFiles(
+		t,
+		TestMode,
+		false,
+		func(router *Engine) {
+			router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
+		},
+	)
+	defer ts.Close()
+
+	res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
+	if err != nil {
+		t.Error(err)
+	}
+
+	resp, _ := io.ReadAll(res.Body)
+	assert.Equal(t, "<h1>Hello world</h1>", string(resp))
+}
+
+func TestLoadHTMLFSDebugMode(t *testing.T) {
+	ts := setupHTMLFiles(
+		t,
+		DebugMode,
+		false,
+		func(router *Engine) {
+			router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
+		},
+	)
+	defer ts.Close()
+
+	res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
+	if err != nil {
+		t.Error(err)
+	}
+
+	resp, _ := io.ReadAll(res.Body)
+	assert.Equal(t, "<h1>Hello world</h1>", string(resp))
+}
+
+func TestLoadHTMLFSReleaseMode(t *testing.T) {
+	ts := setupHTMLFiles(
+		t,
+		ReleaseMode,
+		false,
+		func(router *Engine) {
+			router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
+		},
+	)
+	defer ts.Close()
+
+	res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
+	if err != nil {
+		t.Error(err)
+	}
+
+	resp, _ := io.ReadAll(res.Body)
+	assert.Equal(t, "<h1>Hello world</h1>", string(resp))
+}
+
+func TestLoadHTMLFSUsingTLS(t *testing.T) {
+	ts := setupHTMLFiles(
+		t,
+		TestMode,
+		true,
+		func(router *Engine) {
+			router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
+		},
+	)
+	defer ts.Close()
+
+	// Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error
+	tr := &http.Transport{
+		TLSClientConfig: &tls.Config{
+			InsecureSkipVerify: true,
+		},
+	}
+	client := &http.Client{Transport: tr}
+	res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
+	if err != nil {
+		t.Error(err)
+	}
+
+	resp, _ := io.ReadAll(res.Body)
+	assert.Equal(t, "<h1>Hello world</h1>", string(resp))
+}
+
+func TestLoadHTMLFSFuncMap(t *testing.T) {
+	ts := setupHTMLFiles(
+		t,
+		TestMode,
+		false,
+		func(router *Engine) {
+			router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
+		},
+	)
+	defer ts.Close()
+
+	res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
+	if err != nil {
+		t.Error(err)
+	}
+
+	resp, _ := io.ReadAll(res.Body)
+	assert.Equal(t, "Date: 2017/07/01", string(resp))
+}
+
 func TestAddRoute(t *testing.T) {
 	router := New()
 	router.addRoute(http.MethodGet, "/", HandlersChain{func(_ *Context) {}})
diff --git a/internal/fs/fs_test.go b/internal/fs/fs_test.go
new file mode 100644
index 0000000..113e92b
--- /dev/null
+++ b/internal/fs/fs_test.go
@@ -0,0 +1,49 @@
+package fs
+
+import (
+	"errors"
+	"net/http"
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+type mockFileSystem struct {
+	open func(name string) (http.File, error)
+}
+
+func (m *mockFileSystem) Open(name string) (http.File, error) {
+	return m.open(name)
+}
+
+func TesFileSystem_Open(t *testing.T) {
+	var testFile *os.File
+	mockFS := &mockFileSystem{
+		open: func(name string) (http.File, error) {
+			return testFile, nil
+		},
+	}
+	fs := &FileSystem{mockFS}
+
+	file, err := fs.Open("foo")
+
+	require.NoError(t, err)
+	assert.Equal(t, testFile, file)
+}
+
+func TestFileSystem_Open_err(t *testing.T) {
+	testError := errors.New("mock")
+	mockFS := &mockFileSystem{
+		open: func(_ string) (http.File, error) {
+			return nil, testError
+		},
+	}
+	fs := &FileSystem{mockFS}
+
+	file, err := fs.Open("foo")
+
+	require.ErrorIs(t, err, testError)
+	assert.Nil(t, file)
+}
diff --git a/render/render_test.go b/render/render_test.go
index ad633b0..4dd2a3a 100644
--- a/render/render_test.go
+++ b/render/render_test.go
@@ -489,10 +489,12 @@ func TestRenderHTMLTemplateEmptyName(t *testing.T) {
 func TestRenderHTMLDebugFiles(t *testing.T) {
 	w := httptest.NewRecorder()
 	htmlRender := HTMLDebug{
-		Files:   []string{"../testdata/template/hello.tmpl"},
-		Glob:    "",
-		Delims:  Delims{Left: "{[{", Right: "}]}"},
-		FuncMap: nil,
+		Files:      []string{"../testdata/template/hello.tmpl"},
+		Glob:       "",
+		FileSystem: nil,
+		Patterns:   nil,
+		Delims:     Delims{Left: "{[{", Right: "}]}"},
+		FuncMap:    nil,
 	}
 	instance := htmlRender.Instance("hello.tmpl", map[string]any{
 		"name": "thinkerou",
@@ -508,10 +510,33 @@ func TestRenderHTMLDebugFiles(t *testing.T) {
 func TestRenderHTMLDebugGlob(t *testing.T) {
 	w := httptest.NewRecorder()
 	htmlRender := HTMLDebug{
-		Files:   nil,
-		Glob:    "../testdata/template/hello*",
-		Delims:  Delims{Left: "{[{", Right: "}]}"},
-		FuncMap: nil,
+		Files:      nil,
+		Glob:       "../testdata/template/hello*",
+		FileSystem: nil,
+		Patterns:   nil,
+		Delims:     Delims{Left: "{[{", Right: "}]}"},
+		FuncMap:    nil,
+	}
+	instance := htmlRender.Instance("hello.tmpl", map[string]any{
+		"name": "thinkerou",
+	})
+
+	err := instance.Render(w)
+
+	require.NoError(t, err)
+	assert.Equal(t, "<h1>Hello thinkerou</h1>", w.Body.String())
+	assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
+}
+
+func TestRenderHTMLDebugFS(t *testing.T) {
+	w := httptest.NewRecorder()
+	htmlRender := HTMLDebug{
+		Files:      nil,
+		Glob:       "",
+		FileSystem: http.Dir("../testdata/template"),
+		Patterns:   []string{"hello.tmpl"},
+		Delims:     Delims{Left: "{[{", Right: "}]}"},
+		FuncMap:    nil,
 	}
 	instance := htmlRender.Instance("hello.tmpl", map[string]any{
 		"name": "thinkerou",
@@ -526,10 +551,12 @@ func TestRenderHTMLDebugGlob(t *testing.T) {
 
 func TestRenderHTMLDebugPanics(t *testing.T) {
 	htmlRender := HTMLDebug{
-		Files:   nil,
-		Glob:    "",
-		Delims:  Delims{"{{", "}}"},
-		FuncMap: nil,
+		Files:      nil,
+		Glob:       "",
+		FileSystem: nil,
+		Patterns:   nil,
+		Delims:     Delims{"{{", "}}"},
+		FuncMap:    nil,
 	}
 	assert.Panics(t, func() { htmlRender.Instance("", nil) })
 }