Repository: cloudwego/hertz
Pull request #243 (2022-09-14 07:18:05)

chore: release v0.3.1


Patch:
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index b603978..4641e32 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,5 +1,4 @@
 #### What type of PR is this?
-
 <!--
 Add one of the following kinds:
 
@@ -11,21 +10,33 @@ optimize: A new optimization
 fix: A bug fix
 perf: A code change that improves performance
 refactor: A code change that neither fixes a bug nor adds a feature
-style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
+style: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc)
 test: Adding missing tests or correcting existing tests
 chore: Changes to the build process or auxiliary tools and libraries such as documentation generation
 -->
 
-#### What this PR does / why we need it (English/Chinese):
+#### Check the PR title.
+<!--
+The description of the title will be attached in Release Notes, 
+so please describe it from user-oriented, what this PR does / why we need it.
+Please check your PR title with the below requirements:
+-->
+- [ ] This PR title match the format: \<type\>(optional scope): \<description\>
+- [ ] The description of this PR title is user-oriented and clear enough for others to understand.
+
 
+#### (Optional) Translate the PR title into Chinese.
+
+
+#### (Optional) More detail description for this PR(en: English/zh: Chinese).
 <!--
-The description will be attached in Release Notes, 
-so please describe it from user-oriented.
+Provide more detailed info for review. If it is a perf type PR, perf data is suggested to give.
 -->
+en:
+zh(optional):
 
 #### Which issue(s) this PR fixes:
-
 <!--
-*Automatically closes linked issue when PR is merged.
+Automatically closes linked issue when PR is merged.
 Eg: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
 -->
diff --git a/examples/html_rendering/index.tmpl b/examples/html_rendering/index.tmpl
new file mode 100644
index 0000000..5e5a261
--- /dev/null
+++ b/examples/html_rendering/index.tmpl
@@ -0,0 +1,5 @@
+<html>
+<h1>
+    {[{ .title }]}
+</h1>
+</html>
\ No newline at end of file
diff --git a/examples/html_rendering/main.go b/examples/html_rendering/main.go
new file mode 100644
index 0000000..ad5c080
--- /dev/null
+++ b/examples/html_rendering/main.go
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2022 CloudWeGo Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main
+
+import (
+	"context"
+	"fmt"
+	"html/template"
+	"time"
+
+	"github.com/cloudwego/hertz/pkg/app"
+	"github.com/cloudwego/hertz/pkg/app/server"
+	"github.com/cloudwego/hertz/pkg/common/utils"
+	"github.com/cloudwego/hertz/pkg/protocol/consts"
+)
+
+func formatAsDate(t time.Time) string {
+	year, month, day := t.Date()
+	return fmt.Sprintf("%d/%02d/%02d", year, month, day)
+}
+
+func main() {
+	// set interval to 0 means using fs-watching mechanism.
+	h := server.Default(server.WithAutoReloadRender(true, 0))
+
+	h.Delims("{[{", "}]}")
+
+	h.SetFuncMap(template.FuncMap{
+		"formatAsDate": formatAsDate,
+	})
+	h.LoadHTMLGlob("./examples/html_rendering/*")
+
+	h.GET("/index", func(c context.Context, ctx *app.RequestContext) {
+		ctx.HTML(consts.StatusOK, "index.tmpl", utils.H{
+			"title": "Main website",
+		})
+	})
+
+	h.GET("/raw", func(c context.Context, ctx *app.RequestContext) {
+		ctx.HTML(consts.StatusOK, "template.html", utils.H{
+			"now": time.Date(2017, 0o7, 0o1, 0, 0, 0, 0, time.UTC),
+		})
+	})
+
+	h.Spin()
+}
diff --git a/examples/html_rendering/template.html b/examples/html_rendering/template.html
new file mode 100644
index 0000000..b37104a
--- /dev/null
+++ b/examples/html_rendering/template.html
@@ -0,0 +1,1 @@
+<h1>Date: {[{.now | formatAsDate}]}</h1>
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 9cf0e61..cf0ca35 100644
--- a/go.mod
+++ b/go.mod
@@ -5,9 +5,10 @@ go 1.16
 require (
 	github.com/bytedance/go-tagexpr/v2 v2.9.2
 	github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7
-	github.com/bytedance/sonic v1.3.0
-	github.com/cloudwego/netpoll v0.2.4
+	github.com/bytedance/sonic v1.3.5
+	github.com/cloudwego/netpoll v0.2.6
+	github.com/fsnotify/fsnotify v1.5.4
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
-	golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe
+	golang.org/x/sys v0.0.0-20220412211240-33da011f77ad
 	google.golang.org/protobuf v1.27.1
 )
diff --git a/go.sum b/go.sum
index 8444b8e..1ed067c 100644
--- a/go.sum
+++ b/go.sum
@@ -2,15 +2,17 @@ github.com/bytedance/go-tagexpr/v2 v2.9.2 h1:QySJaAIQgOEDQBLS3x9BxOWrnhqu5sQ+f6H
 github.com/bytedance/go-tagexpr/v2 v2.9.2/go.mod h1:5qsx05dYOiUXOUgnQ7w3Oz8BYs2qtM/bJokdLb79wRM=
 github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7 h1:PtwsQyQJGxf8iaPptPNaduEIu9BnrNms+pcRdHAxZaM=
 github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q=
-github.com/bytedance/sonic v1.3.0 h1:T2rlvNytw6bTmczlAXvGqmuMzIqGJBOsJKYwRPWR7Y8=
-github.com/bytedance/sonic v1.3.0/go.mod h1:V973WhNhGmvHxW6nQmsHEfHaoU9F3zTF+93rH03hcUQ=
+github.com/bytedance/sonic v1.3.5 h1:xfBNhsG3QCC+AMCmCHxNQg0StI5IM/B9Jtwjqi5WlI0=
+github.com/bytedance/sonic v1.3.5/go.mod h1:V973WhNhGmvHxW6nQmsHEfHaoU9F3zTF+93rH03hcUQ=
 github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06 h1:1sDoSuDPWzhkdzNVxCxtIaKiAe96ESVPv8coGwc1gZ4=
 github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
-github.com/cloudwego/netpoll v0.2.4 h1:Kbo2HA1cXEgoy/bu1jSNrjcqZj2diENcJqLy6vKiROU=
-github.com/cloudwego/netpoll v0.2.4/go.mod h1:1T2WVuQ+MQw6h6DpE45MohSvDTKdy2DlzCx2KsnPI4E=
+github.com/cloudwego/netpoll v0.2.6 h1:vzN8cyayoa9RdCOG87tqkYO/j2hA4SMLC+vkcNUq6uI=
+github.com/cloudwego/netpoll v0.2.6/go.mod h1:1T2WVuQ+MQw6h6DpE45MohSvDTKdy2DlzCx2KsnPI4E=
 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/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
+github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
 github.com/goccy/go-json v0.9.4 h1:L8MLKG2mvVXiQu07qB6hmfqeSYQdOnqPot2GhsIwIaI=
 github.com/goccy/go-json v0.9.4/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -58,8 +60,9 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VA
 golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe h1:W8vbETX/n8S6EmY0Pu4Ix7VvpsJUESTwl0oCK8MJOgk=
 golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
diff --git a/pkg/app/context.go b/pkg/app/context.go
index 8409e5e..b146425 100644
--- a/pkg/app/context.go
+++ b/pkg/app/context.go
@@ -47,6 +47,7 @@ import (
 	"io"
 	"mime/multipart"
 	"net"
+	"net/url"
 	"os"
 	"reflect"
 	"strings"
@@ -950,6 +951,40 @@ func (ctx *RequestContext) Cookie(key string) []byte {
 	return ctx.Request.Header.Cookie(key)
 }
 
+// SetCookie adds a Set-Cookie header to the Response's headers.
+//  Parameter introduce:
+//  name and value is used to set cookie's name and value, eg. Set-Cookie: name=value
+//  maxAge is use to set cookie's expiry date, eg. Set-Cookie: name=value; max-age=1
+//  path and domain is used to set the scope of a cookie, eg. Set-Cookie: name=value;domain=localhost; path=/;
+//  secure and httpOnly is used to sent cookies securely; eg. Set-Cookie: name=value;HttpOnly; secure;
+//  sameSite let servers specify whether/when cookies are sent with cross-site requests; eg. Set-Cookie: name=value;HttpOnly; secure; SameSite=Lax;
+//
+//  For example:
+//  1. ctx.SetCookie("user", "hertz", 1, "/", "localhost",protocol.CookieSameSiteLaxMode, true, true)
+//  add response header --->  Set-Cookie: user=hertz; max-age=1; domain=localhost; path=/; HttpOnly; secure; SameSite=Lax;
+//  2. ctx.SetCookie("user", "hertz", 10, "/", "localhost",protocol.CookieSameSiteLaxMode, false, false)
+//  add response header --->  Set-Cookie: user=hertz; max-age=10; domain=localhost; path=/; SameSite=Lax;
+//  3. ctx.SetCookie("", "hertz", 10, "/", "localhost",protocol.CookieSameSiteLaxMode, false, false)
+//  add response header --->  Set-Cookie: hertz; max-age=10; domain=localhost; path=/; SameSite=Lax;
+//  4. ctx.SetCookie("user", "", 10, "/", "localhost",protocol.CookieSameSiteLaxMode, false, false)
+//  add response header --->  Set-Cookie: user=; max-age=10; domain=localhost; path=/; SameSite=Lax;
+func (ctx *RequestContext) SetCookie(name, value string, maxAge int, path, domain string, sameSite protocol.CookieSameSite, secure, httpOnly bool) {
+	if path == "" {
+		path = "/"
+	}
+	cookie := protocol.AcquireCookie()
+	defer protocol.ReleaseCookie(cookie)
+	cookie.SetKey(name)
+	cookie.SetValue(url.QueryEscape(value))
+	cookie.SetMaxAge(maxAge)
+	cookie.SetPath(path)
+	cookie.SetDomain(domain)
+	cookie.SetSecure(secure)
+	cookie.SetHTTPOnly(httpOnly)
+	cookie.SetSameSite(sameSite)
+	ctx.Response.Header.SetCookie(cookie)
+}
+
 // UserAgent returns the value of the request user_agent.
 func (ctx *RequestContext) UserAgent() []byte {
 	return ctx.Request.Header.UserAgent()
diff --git a/pkg/app/server/option.go b/pkg/app/server/option.go
index c67ddcc..ccfa97a 100644
--- a/pkg/app/server/option.go
+++ b/pkg/app/server/option.go
@@ -277,3 +277,14 @@ func WithRegistry(r registry.Registry, info *registry.Info) config.Option {
 		o.RegistryInfo = info
 	}}
 }
+
+// WithAutoReloadRender sets the config of auto reload render.
+// If auto reload render is enabled:
+// 1. interval = 0 means reload render according to file watch mechanism.(recommended)
+// 2. interval > 0 means reload render every interval.
+func WithAutoReloadRender(b bool, interval time.Duration) config.Option {
+	return config.Option{F: func(o *config.Options) {
+		o.AutoReloadRender = b
+		o.AutoReloadInterval = interval
+	}}
+}
diff --git a/pkg/app/server/render/html.go b/pkg/app/server/render/html.go
index 12f503f..03cf687 100644
--- a/pkg/app/server/render/html.go
+++ b/pkg/app/server/render/html.go
@@ -43,8 +43,13 @@ package render
 
 import (
 	"html/template"
+	"log"
+	"sync"
+	"time"
 
+	"github.com/cloudwego/hertz/pkg/common/hlog"
 	"github.com/cloudwego/hertz/pkg/protocol"
+	"github.com/fsnotify/fsnotify"
 )
 
 // Delims represents a set of Left and Right delimiters for HTML template rendering.
@@ -59,12 +64,12 @@ type Delims struct {
 type HTMLRender interface {
 	// Instance returns an HTML instance.
 	Instance(string, interface{}) Render
+	Close() error
 }
 
 // HTMLProduction contains template reference and its delims.
 type HTMLProduction struct {
 	Template *template.Template
-	Delims   Delims
 }
 
 // HTML contains template reference and its name with given interface object.
@@ -85,6 +90,10 @@ func (r HTMLProduction) Instance(name string, data interface{}) Render {
 	}
 }
 
+func (r HTMLProduction) Close() error {
+	return nil
+}
+
 // Render (HTML) executes template and writes its result with custom ContentType for response.
 func (r HTML) Render(resp *protocol.Response) error {
 	r.WriteContentType(resp)
@@ -99,3 +108,105 @@ func (r HTML) Render(resp *protocol.Response) error {
 func (r HTML) WriteContentType(resp *protocol.Response) {
 	writeContentType(resp, htmlContentType)
 }
+
+type HTMLDebug struct {
+	sync.Once
+	Template        *template.Template
+	RefreshInterval time.Duration
+	updateTimeStamp time.Time
+
+	Files   []string
+	FuncMap template.FuncMap
+	Delims  Delims
+
+	reloadCh chan struct{}
+	watcher  *fsnotify.Watcher
+}
+
+func (h *HTMLDebug) Instance(name string, data interface{}) Render {
+	h.Do(func() {
+		h.startChecker()
+	})
+
+	select {
+	case <-h.reloadCh:
+		h.reload()
+	default:
+	}
+
+	return HTML{
+		Template: h.Template,
+		Name:     name,
+		Data:     data,
+	}
+}
+
+func (h *HTMLDebug) Close() error {
+	if h.watcher == nil {
+		return nil
+	}
+	return h.watcher.Close()
+}
+
+func (h *HTMLDebug) reload() {
+	h.Template = template.Must(template.New("").
+		Delims(h.Delims.Left, h.Delims.Right).
+		Funcs(h.FuncMap).
+		ParseFiles(h.Files...))
+}
+
+func (h *HTMLDebug) startChecker() {
+	h.reloadCh = make(chan struct{})
+
+	if h.RefreshInterval > 0 {
+		go func() {
+			hlog.Debugf("HERTZ[HTMLDebug]: HTML template reloader started with interval %v", h.RefreshInterval)
+			for {
+				n := time.Now()
+				if n.UTC().Sub(h.updateTimeStamp.UTC()) > h.RefreshInterval {
+					hlog.Debugf("HERTZ[HTMLDebug]: triggering HTML template reloader")
+					h.reloadCh <- struct{}{}
+					hlog.Debugf("HERTZ[HTMLDebug]: HTML template has been reloaded, next reload in %v", h.RefreshInterval)
+					h.updateTimeStamp = time.Now()
+				}
+			}
+		}()
+		return
+	}
+
+	watcher, err := fsnotify.NewWatcher()
+	if err != nil {
+		log.Fatal(err)
+	}
+	h.watcher = watcher
+	for _, f := range h.Files {
+		err := watcher.Add(f)
+		hlog.Debugf("HERTZ[HTMLDebug]: watching file: %s", f)
+		if err != nil {
+			hlog.Errorf("HERTZ[HTMLDebug]: add watching file: %s, error happened: %v", f, err)
+		}
+
+	}
+
+	go func() {
+		hlog.Debugf("HERTZ[HTMLDebug]: HTML template reloader started with file watcher")
+		for {
+			select {
+			case event, ok := <-watcher.Events:
+				if !ok {
+					return
+				}
+				if event.Op&fsnotify.Write == fsnotify.Write {
+					hlog.Debugf("HERTZ[HTMLDebug]: modified file: %s, html render template will be reloaded at the next rendering", event.Name)
+					h.reloadCh <- struct{}{}
+					hlog.Debugf("HERTZ[HTMLDebug]: HTML template has been reloaded")
+				}
+			case err, ok := <-watcher.Errors:
+				if !ok {
+					return
+				}
+				hlog.Errorf("HERTZ: error happened when watching the rendering files: %v", err)
+			}
+		}
+	}()
+}
diff --git a/pkg/common/config/option.go b/pkg/common/config/option.go
index 868b476..1fc2789 100644
--- a/pkg/common/config/option.go
+++ b/pkg/common/config/option.go
@@ -71,6 +71,12 @@ type Options struct {
 	Registry registry.Registry
 	// RegistryInfo is base info used for service registry.
 	RegistryInfo *registry.Info
+	// Enable automatically HTML template reloading mechanism.
+	AutoReloadRender bool
+	// If AutoReloadInterval is set to 0(default).
+	// The HTML template will reload according to files' changing event
+	// otherwise it will reload after AutoReloadInterval.
+	AutoReloadInterval time.Duration
 }
 
 func (o *Options) Apply(opts []Option) {
diff --git a/pkg/protocol/client/client.go b/pkg/protocol/client/client.go
index 962abfb..e5e6cf1 100644
--- a/pkg/protocol/client/client.go
+++ b/pkg/protocol/client/client.go
@@ -43,7 +43,6 @@ package client
 
 import (
 	"context"
-	"crypto/tls"
 	"sync"
 	"time"
 
@@ -79,34 +78,6 @@ type Doer interface {
 	Do(ctx context.Context, req *protocol.Request, resp *protocol.Response) error
 }
 
-type HostClientConfig struct {
-	DynamicConfig
-
-	Name string
-
-	NoDefaultUserAgentHeader      bool
-	DialDualStack                 bool
-	DisableHeaderNamesNormalizing bool
-	DisablePathNormalizing        bool
-	IsTLS                         bool
-
-	TLSConfig *tls.Config
-
-	MaxConns                  int
-	MaxIdempotentCallAttempts int
-	MaxResponseBodySize       int
-
-	RetryIf            RetryIfFunc
-	ResponseBodyStream bool
-
-	DialTimeout         time.Duration
-	MaxIdleConnDuration time.Duration
-	MaxConnDuration     time.Duration
-	ReadTimeout         time.Duration
-	WriteTimeout        time.Duration
-	MaxConnWaitTimeout  time.Duration
-}
-
 // DynamicConfig is config set which will be confirmed when starts a request.
 type DynamicConfig struct {
 	Addr     string
diff --git a/pkg/protocol/http1/client.go b/pkg/protocol/http1/client.go
index 12d2ccc..4be7be1 100644
--- a/pkg/protocol/http1/client.go
+++ b/pkg/protocol/http1/client.go
@@ -90,6 +90,19 @@ type HostClient struct {
 
 	*ClientOptions
 
+	// Comma-separated list of upstream HTTP server host addresses,
+	// which are passed to Dialer in a round-robin manner.
+	//
+	// Each address may contain port if default dialer is used.
+	// For example,
+	//
+	//    - foobar.com:80
+	//    - foobar.com:443
+	//    - foobar.com:8080
+	Addr     string
+	IsTLS    bool
+	ProxyURI *protocol.URI
+
 	clientName  atomic.Value
 	lastUseTime uint32
 
@@ -1044,17 +1057,6 @@ func NewHostClient(c *ClientOptions) client.HostClient {
 }
 
 type ClientOptions struct {
-	// Comma-separated list of upstream HTTP server host addresses,
-	// which are passed to Dialer in a round-robin manner.
-	//
-	// Each address may contain port if default dialer is used.
-	// For example,
-	//
-	//    - foobar.com:80
-	//    - foobar.com:443
-	//    - foobar.com:8080
-	Addr string
-
 	// Client name. Used in User-Agent request header.
 	Name string
 
@@ -1086,8 +1088,6 @@ type ClientOptions struct {
 	// Optional TLS config.
 	TLSConfig *tls.Config
 
-	IsTLS bool
-
 	// Maximum number of connections which may be established to all hosts
 	// listed in Addr.
 	//
@@ -1168,6 +1168,4 @@ type ClientOptions struct {
 
 	// ResponseBodyStream enables response body streaming
 	ResponseBodyStream bool
-
-	ProxyURI *protocol.URI
 }
diff --git a/pkg/protocol/uri.go b/pkg/protocol/uri.go
index 2f8fefa..6bba7a9 100644
--- a/pkg/protocol/uri.go
+++ b/pkg/protocol/uri.go
@@ -43,6 +43,7 @@ package protocol
 
 import (
 	"bytes"
+	"path/filepath"
 	"sync"
 
 	"github.com/cloudwego/hertz/internal/bytesconv"
@@ -318,9 +319,9 @@ func (u *URI) SetHostBytes(host []byte) {
 //
 // Examples:
 //
-//    * For /foo/bar/baz.html path returns baz.html.
-//    * For /foo/bar/ returns empty byte slice.
-//    * For /foobar.js returns foobar.js.
+//   - For /foo/bar/baz.html path returns baz.html.
+//   - For /foo/bar/ returns empty byte slice.
+//   - For /foobar.js returns foobar.js.
 func (u *URI) LastPathSegment() []byte {
 	path := u.Path()
 	n := bytes.LastIndexByte(path, '/')
@@ -334,14 +335,14 @@ func (u *URI) LastPathSegment() []byte {
 //
 // The following newURI types are accepted:
 //
-//     * Absolute, i.e. http://foobar.com/aaa/bb?cc . In this case the original
-//       uri is replaced by newURI.
-//     * Absolute without scheme, i.e. //foobar.com/aaa/bb?cc. In this case
-//       the original scheme is preserved.
-//     * Missing host, i.e. /aaa/bb?cc . In this case only RequestURI part
-//       of the original uri is replaced.
-//     * Relative path, i.e.  xx?yy=abc . In this case the original RequestURI
-//       is updated according to the new relative path.
+//   - Absolute, i.e. http://foobar.com/aaa/bb?cc . In this case the original
+//     uri is replaced by newURI.
+//   - Absolute without scheme, i.e. //foobar.com/aaa/bb?cc. In this case
+//     the original scheme is preserved.
+//   - Missing host, i.e. /aaa/bb?cc . In this case only RequestURI part
+//     of the original uri is replaced.
+//   - Relative path, i.e.  xx?yy=abc . In this case the original RequestURI
+//     is updated according to the new relative path.
 func (u *URI) Update(newURI string) {
 	u.UpdateBytes(bytesconv.S2b(newURI))
 }
@@ -350,14 +351,14 @@ func (u *URI) Update(newURI string) {
 //
 // The following newURI types are accepted:
 //
-//     * Absolute, i.e. http://foobar.com/aaa/bb?cc . In this case the original
-//       uri is replaced by newURI.
-//     * Absolute without scheme, i.e. //foobar.com/aaa/bb?cc. In this case
-//       the original scheme is preserved.
-//     * Missing host, i.e. /aaa/bb?cc . In this case only RequestURI part
-//       of the original uri is replaced.
-//     * Relative path, i.e.  xx?yy=abc . In this case the original RequestURI
-//       is updated according to the new relative path.
+//   - Absolute, i.e. http://foobar.com/aaa/bb?cc . In this case the original
+//     uri is replaced by newURI.
+//   - Absolute without scheme, i.e. //foobar.com/aaa/bb?cc. In this case
+//     the original scheme is preserved.
+//   - Missing host, i.e. /aaa/bb?cc . In this case only RequestURI part
+//     of the original uri is replaced.
+//   - Relative path, i.e.  xx?yy=abc . In this case the original RequestURI
+//     is updated according to the new relative path.
 func (u *URI) UpdateBytes(newURI []byte) {
 	u.requestURI = u.updateBytes(newURI, u.requestURI)
 }
@@ -484,6 +485,18 @@ func normalizePath(dst, src []byte) []byte {
 	dst = addLeadingSlash(dst, src)
 	dst = decodeArgAppendNoPlus(dst, src)
 
+	// Windows server need to replace all backslashes with
+	// forward slashes to avoid path traversal attacks.
+	if filepath.Separator == '\\' {
+		for {
+			n := bytes.IndexByte(dst, '\\')
+			if n < 0 {
+				break
+			}
+			dst[n] = '/'
+		}
+	}
+
 	// remove duplicate slashes
 	b := dst
 	bSize := len(b)
diff --git a/pkg/route/engine.go b/pkg/route/engine.go
index 618d19e..06f1f62 100644
--- a/pkg/route/engine.go
+++ b/pkg/route/engine.go
@@ -47,6 +47,7 @@ import (
 	"fmt"
 	"html/template"
 	"io"
+	"path/filepath"
 	"reflect"
 	"runtime"
 	"strings"
@@ -419,6 +420,9 @@ func getRemoteAddrFromCloser(conn io.Closer) string {
 }
 
 func (engine *Engine) Close() error {
+	if engine.htmlRender != nil {
+		engine.htmlRender.Close() //nolint:errcheck
+	}
 	return engine.transport.Close()
 }
 
@@ -754,23 +758,54 @@ func (engine *Engine) Use(middleware ...app.HandlerFunc) IRoutes {
 // LoadHTMLGlob loads HTML files identified by glob pattern
 // and associates the result with HTML renderer.
 func (engine *Engine) LoadHTMLGlob(pattern string) {
-	left := engine.delims.Left
-	right := engine.delims.Right
-	templ := template.Must(template.New("").Delims(left, right).Funcs(engine.funcMap).ParseGlob(pattern))
+	tmpl := template.Must(template.New("").
+		Delims(engine.delims.Left, engine.delims.Right).
+		Funcs(engine.funcMap).
+		ParseGlob(pattern))
+
+	if engine.options.AutoReloadRender {
+		files, err := filepath.Glob(pattern)
+		if err != nil {
+			hlog.Errorf("LoadHTMLGlob: %v", err)
+			return
+		}
+		engine.SetAutoReloadHTMLTemplate(tmpl, files)
+		return
+	}
 
-	engine.SetHTMLTemplate(templ)
+	engine.SetHTMLTemplate(tmpl)
 }
 
 // LoadHTMLFiles loads a slice of HTML files
 // and associates the result with HTML renderer.
 func (engine *Engine) LoadHTMLFiles(files ...string) {
-	templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.funcMap).ParseFiles(files...))
-	engine.SetHTMLTemplate(templ)
+	tmpl := template.Must(template.New("").
+		Delims(engine.delims.Left, engine.delims.Right).
+		Funcs(engine.funcMap).
+		ParseFiles(files...))
+
+	if engine.options.AutoReloadRender {
+		engine.SetAutoReloadHTMLTemplate(tmpl, files)
+		return
+	}
+
+	engine.SetHTMLTemplate(tmpl)
 }
 
 // SetHTMLTemplate associate a template with HTML renderer.
-func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
-	engine.htmlRender = render.HTMLProduction{Template: templ.Funcs(engine.funcMap)}
+func (engine *Engine) SetHTMLTemplate(tmpl *template.Template) {
+	engine.htmlRender = render.HTMLProduction{Template: tmpl.Funcs(engine.funcMap)}
+}
+
+// SetAutoReloadHTMLTemplate associate a template with HTML renderer.
+func (engine *Engine) SetAutoReloadHTMLTemplate(tmpl *template.Template, files []string) {
+	engine.htmlRender = &render.HTMLDebug{
+		Template:        tmpl,
+		Files:           files,
+		FuncMap:         engine.funcMap,
+		Delims:          engine.delims,
+		RefreshInterval: engine.options.AutoReloadInterval,
+	}
 }
 
 // SetFuncMap sets the funcMap used for template.funcMap.
diff --git a/version.go b/version.go
index d71e454..8119d30 100644
--- a/version.go
+++ b/version.go
@@ -19,5 +19,5 @@ package hertz
 // Name and Version info of this framework, used for statistics and debug
 const (
 	Name    = "Hertz"
-	Version = "v0.3.0"
+	Version = "v0.3.1"
 )


Test patch:
diff --git a/internal/bytestr/bytes.go b/internal/bytestr/bytes.go
index 2cfc0c1..85e488b 100644
--- a/internal/bytestr/bytes.go
+++ b/internal/bytestr/bytes.go
@@ -28,6 +28,7 @@ var (
 )
 
 var (
+	StrBackSlash        = []byte("\\")
 	StrSlash            = []byte("/")
 	StrSlashSlash       = []byte("//")
 	StrSlashDotDot      = []byte("/..")
diff --git a/pkg/app/client/client_test.go b/pkg/app/client/client_test.go
index 0efe406..20c25e7 100644
--- a/pkg/app/client/client_test.go
+++ b/pkg/app/client/client_test.go
@@ -392,8 +392,8 @@ func TestClientReadTimeout(t *testing.T) {
 			ReadTimeout:               time.Second * 4,
 			MaxIdempotentCallAttempts: 1,
 			Dialer:                    standard.NewDialer(),
-			Addr:                      opt.Addr,
 		},
+		Addr: opt.Addr,
 	}
 
 	req := protocol.AcquireRequest()
@@ -689,9 +689,9 @@ func TestHostClientPendingRequests(t *testing.T) {
 
 	c := &http1.HostClient{
 		ClientOptions: &http1.ClientOptions{
-			Addr:   "foobar",
 			Dialer: newMockDialerWithCustomFunc(opt.Network, opt.Addr, time.Second, nil),
 		},
+		Addr: "foobar",
 	}
 
 	pendingRequests := c.PendingRequests()
@@ -780,10 +780,10 @@ func TestHostClientMaxConnsWithDeadline(t *testing.T) {
 
 	c := &http1.HostClient{
 		ClientOptions: &http1.ClientOptions{
-			Addr:     "foobar",
 			Dialer:   newMockDialerWithCustomFunc(opt.Network, opt.Addr, time.Second, nil),
 			MaxConns: 1,
 		},
+		Addr: "foobar",
 	}
 
 	for i := 0; i < 5; i++ {
@@ -849,10 +849,10 @@ func TestHostClientMaxConnDuration(t *testing.T) {
 
 	c := &http1.HostClient{
 		ClientOptions: &http1.ClientOptions{
-			Addr:            "foobar",
 			Dialer:          newMockDialerWithCustomFunc(opt.Network, opt.Addr, time.Second, nil),
 			MaxConnDuration: 10 * time.Millisecond,
 		},
+		Addr: "foobar",
 	}
 
 	for i := 0; i < 5; i++ {
@@ -895,11 +895,11 @@ func TestHostClientMultipleAddrs(t *testing.T) {
 	dialsCount := make(map[string]int)
 	c := &http1.HostClient{
 		ClientOptions: &http1.ClientOptions{
-			Addr: "foo,bar,baz",
 			Dialer: newMockDialerWithCustomFunc(opt.Network, opt.Addr, 1*time.Second, func(network, addr string, timeout time.Duration, tlsConfig *tls.Config) {
 				dialsCount[addr]++
 			}),
 		},
+		Addr: "foo,bar,baz",
 	}
 
 	for i := 0; i < 9; i++ {
@@ -961,9 +961,9 @@ func TestClientFollowRedirects(t *testing.T) {
 
 	c := &http1.HostClient{
 		ClientOptions: &http1.ClientOptions{
-			Addr:   "xxx",
 			Dialer: newMockDialerWithCustomFunc(opt.Network, opt.Addr, 1*time.Second, nil),
 		},
+		Addr: "xxx",
 	}
 
 	for i := 0; i < 10; i++ {
@@ -1055,11 +1055,11 @@ func TestHostClientMaxConnWaitTimeoutSuccess(t *testing.T) {
 
 	c := &http1.HostClient{
 		ClientOptions: &http1.ClientOptions{
-			Addr:               "foobar",
 			Dialer:             newMockDialerWithCustomFunc(opt.Network, opt.Addr, time.Second, nil),
 			MaxConns:           1,
 			MaxConnWaitTimeout: 200 * time.Millisecond,
 		},
+		Addr: "foobar",
 	}
 
 	for i := 0; i < 5; i++ {
@@ -1124,11 +1124,11 @@ func TestHostClientMaxConnWaitTimeoutError(t *testing.T) {
 
 	c := &http1.HostClient{
 		ClientOptions: &http1.ClientOptions{
-			Addr:               "foobar",
 			Dialer:             newMockDialerWithCustomFunc(opt.Network, opt.Addr, time.Second, nil),
 			MaxConns:           1,
 			MaxConnWaitTimeout: 10 * time.Millisecond,
 		},
+		Addr: "foobar",
 	}
 
 	var errNoFreeConnsCount uint32
diff --git a/pkg/app/context_test.go b/pkg/app/context_test.go
index 0a4202b..c83ee6e 100644
--- a/pkg/app/context_test.go
+++ b/pkg/app/context_test.go
@@ -919,3 +919,15 @@ func TestBindAndValidate(t *testing.T) {
 		t.Fatalf("unexpected nil, expected an error")
 	}
 }
+
+func TestRequestContext_SetCookie(t *testing.T) {
+	c := NewContext(0)
+	c.SetCookie("user", "hertz", 1, "/", "localhost", protocol.CookieSameSiteLaxMode, true, true)
+	assert.DeepEqual(t, "user=hertz; max-age=1; domain=localhost; path=/; HttpOnly; secure; SameSite=Lax", c.Response.Header.Get("Set-Cookie"))
+}
+
+func TestRequestContext_SetCookiePathEmpty(t *testing.T) {
+	c := NewContext(0)
+	c.SetCookie("user", "hertz", 1, "", "localhost", protocol.CookieSameSiteDisabled, true, true)
+	assert.DeepEqual(t, "user=hertz; max-age=1; domain=localhost; path=/; HttpOnly; secure", c.Response.Header.Get("Set-Cookie"))
+}
diff --git a/pkg/app/server/option_test.go b/pkg/app/server/option_test.go
index 4f3a885..f45eb22 100644
--- a/pkg/app/server/option_test.go
+++ b/pkg/app/server/option_test.go
@@ -56,6 +56,7 @@ func TestOptions(t *testing.T) {
 		WithALPN(true),
 		WithTraceLevel(stats.LevelDisabled),
 		WithRegistry(nil, info),
+		WithAutoReloadRender(true, 5*time.Second),
 	})
 	assert.DeepEqual(t, opt.ReadTimeout, time.Second)
 	assert.DeepEqual(t, opt.IdleTimeout, time.Second)
@@ -79,6 +80,8 @@ func TestOptions(t *testing.T) {
 	assert.DeepEqual(t, opt.TraceLevel, stats.LevelDisabled)
 	assert.DeepEqual(t, opt.RegistryInfo, info)
 	assert.DeepEqual(t, opt.Registry, nil)
+	assert.DeepEqual(t, opt.AutoReloadRender, true)
+	assert.DeepEqual(t, opt.AutoReloadInterval, 5*time.Second)
 }
 
 func TestDefaultOptions(t *testing.T) {
@@ -103,5 +106,8 @@ func TestDefaultOptions(t *testing.T) {
 	assert.DeepEqual(t, opt.ReadBufferSize, 4096)
 	assert.DeepEqual(t, opt.ALPN, false)
 	assert.DeepEqual(t, opt.Registry, registry.NoopRegistry)
+	assert.DeepEqual(t, opt.AutoReloadRender, false)
 	assert.Assert(t, opt.RegistryInfo == nil)
+	assert.DeepEqual(t, opt.AutoReloadRender, false)
+	assert.DeepEqual(t, opt.AutoReloadInterval, time.Duration(0))
 }
diff --git a/pkg/app/server/render/html_test.go b/pkg/app/server/render/html_test.go
new file mode 100644
index 0000000..86fa981
--- /dev/null
+++ b/pkg/app/server/render/html_test.go
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2022 CloudWeGo Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package render
+
+import (
+	"os"
+	"testing"
+	"time"
+)
+
+func TestHTMLDebug_StartChecker_timer(t *testing.T) {
+	render := &HTMLDebug{RefreshInterval: 2 * time.Second}
+	select {
+	case <-render.reloadCh:
+		t.Fatalf("should not be triggered")
+	default:
+	}
+	render.startChecker()
+	select {
+	case <-time.After(50 * time.Millisecond):
+		t.Fatalf("should be triggered immediately")
+	case <-render.reloadCh:
+
+	}
+	select {
+	// some ci servers have poor computing power, so we add some extra time here.
+	case <-time.After(2*time.Second + 50*time.Millisecond):
+		t.Fatalf("should be triggered before 2 seconds")
+	case <-render.reloadCh:
+		t.Logf("paas")
+	}
+}
+
+func TestHTMLDebug_StartChecker_fs_watcher(t *testing.T) {
+	f, _ := os.CreateTemp("./", "test.tmpl")
+	defer func() {
+		f.Close()
+		os.Remove(f.Name())
+	}()
+	render := &HTMLDebug{Files: []string{f.Name()}}
+	select {
+	case <-render.reloadCh:
+		t.Fatalf("should not be triggered")
+	default:
+	}
+	render.startChecker()
+	f.Write([]byte("hello"))
+
+	select {
+	case <-time.After(50 * time.Millisecond):
+		t.Fatalf("should be triggered immediately")
+	case <-render.reloadCh:
+	}
+	select {
+	case <-render.reloadCh:
+		t.Fatalf("should not be triggered")
+	default:
+	}
+}
diff --git a/pkg/protocol/http1/client_test.go b/pkg/protocol/http1/client_test.go
index ae0871a..299a028 100644
--- a/pkg/protocol/http1/client_test.go
+++ b/pkg/protocol/http1/client_test.go
@@ -74,13 +74,13 @@ func TestHostClientMaxConnWaitTimeoutWithEarlierDeadline(t *testing.T) {
 
 	c := &HostClient{
 		ClientOptions: &ClientOptions{
-			Addr: "foobar",
 			Dialer: newSlowConnDialer(func(network, addr string) (network.Conn, error) {
 				return mock.SlowReadDialer(addr)
 			}),
 			MaxConns:           1,
 			MaxConnWaitTimeout: 50 * time.Millisecond,
 		},
+		Addr: "foobar",
 	}
 
 	var errTimeoutCount uint32
diff --git a/pkg/protocol/uri_test.go b/pkg/protocol/uri_test.go
index fa37131..9da05a7 100644
--- a/pkg/protocol/uri_test.go
+++ b/pkg/protocol/uri_test.go
@@ -43,6 +43,7 @@ package protocol
 
 import (
 	"fmt"
+	"path/filepath"
 	"reflect"
 	"testing"
 
@@ -228,3 +229,20 @@ func testURIFullURI(t *testing.T, scheme, host, path, hash string, args *Args, e
 		t.Fatalf("Unexpected URI: %q. Expected %q", uri, expectedURI)
 	}
 }
+
+func TestParsePathWindows(t *testing.T) {
+	t.Parallel()
+
+	testParsePathWindows(t, "/../../../../../foo", "/foo")
+	testParsePathWindows(t, "/..\\..\\..\\..\\..\\foo", "/foo")
+	testParsePathWindows(t, "/..%5c..%5cfoo", "/foo")
+}
+
+func testParsePathWindows(t *testing.T, path, expectedPath string) {
+	var u URI
+	u.Parse(nil, []byte(path))
+	parsedPath := u.Path()
+	if filepath.Separator == '\\' && string(parsedPath) != expectedPath {
+		t.Fatalf("Unexpected Path: %q. Expected %q", parsedPath, expectedPath)
+	}
+}
diff --git a/pkg/route/engine_test.go b/pkg/route/engine_test.go
index c646658..9b99180 100644
--- a/pkg/route/engine_test.go
+++ b/pkg/route/engine_test.go
@@ -391,6 +391,48 @@ func (m *mockTransporter) Shutdown(ctx context.Context) error {
 	panic("implement me")
 }
 
+func TestRenderHtmlOfGlobWithAutoRender(t *testing.T) {
+	opt := config.NewOptions([]config.Option{})
+	opt.AutoReloadRender = true
+	e := NewEngine(opt)
+	e.Delims("{[{", "}]}")
+	e.SetFuncMap(template.FuncMap{
+		"formatAsDate": formatAsDate,
+	})
+	e.LoadHTMLGlob("../common/testdata/template/htmltemplate.html")
+	e.GET("/templateName", func(c context.Context, ctx *app.RequestContext) {
+		ctx.HTML(http.StatusOK, "htmltemplate.html", map[string]interface{}{
+			"now": time.Date(2017, 0o7, 0o1, 0, 0, 0, 0, time.UTC),
+		})
+	})
+	rr := performRequest(e, "GET", "/templateName")
+	b, _ := ioutil.ReadAll(rr.Body)
+	assert.DeepEqual(t, 200, rr.Code)
+	assert.DeepEqual(t, []byte("<h1>Date: 2017/07/01</h1>"), b)
+	assert.DeepEqual(t, "text/html; charset=utf-8", rr.Header().Get("Content-Type"))
+}
+
+func TestRenderHtmlOfFilesWithAutoRender(t *testing.T) {
+	opt := config.NewOptions([]config.Option{})
+	opt.AutoReloadRender = true
+	e := NewEngine(opt)
+	e.Delims("{[{", "}]}")
+	e.SetFuncMap(template.FuncMap{
+		"formatAsDate": formatAsDate,
+	})
+	e.LoadHTMLFiles("../common/testdata/template/htmltemplate.html")
+	e.GET("/templateName", func(c context.Context, ctx *app.RequestContext) {
+		ctx.HTML(http.StatusOK, "htmltemplate.html", map[string]interface{}{
+			"now": time.Date(2017, 0o7, 0o1, 0, 0, 0, 0, time.UTC),
+		})
+	})
+	rr := performRequest(e, "GET", "/templateName")
+	b, _ := ioutil.ReadAll(rr.Body)
+	assert.DeepEqual(t, 200, rr.Code)
+	assert.DeepEqual(t, []byte("<h1>Date: 2017/07/01</h1>"), b)
+	assert.DeepEqual(t, "text/html; charset=utf-8", rr.Header().Get("Content-Type"))
+}
+
 type mockConn struct{}
 
 func (m *mockConn) ReadBinary(n int) (p []byte, err error) {