Repository: cloudwego/hertz (go)

Issue:
Disable Default Content-Type for HTTP Requests
**Is your feature request related to a problem? Please describe.**

When sending a HTTP request, hertz will always send a `Content-Type` header, if the value was not specified by the user, it will default to `application/x-www-form-urlencoded`.

**Describe the solution you'd like**

I think the solution posed in this fasthttp issue comment is reasonable. https://github.com/valyala/fasthttp/issues/873#issuecomment-688947641
> For ResponseHeader we have [ResponseHeader.SetNoDefaultContentType()](https://godoc.org/github.com/valyala/fasthttp#ResponseHeader.SetNoDefaultContentType). I think we should add something similar for RequestHeader.

**Additional context**

Related issues in fasthttp:
https://github.com/valyala/fasthttp/issues/873
https://github.com/valyala/fasthttp/pull/1023

Pull request description:
feat: add RequestHeader.SetNoDefaultContentType
#### What type of PR is this?
feat
<!--
Add one of the following kinds:

build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
docs: Documentation only changes
feat: A new feature
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)
test: Adding missing tests or correcting existing tests
chore: Changes to the build process or auxiliary tools and libraries such as documentation generation
-->

#### 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:
-->
- [x] This PR title match the format: \<type\>(optional scope): \<description\>
- [x] 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).
<!--
Provide more detailed info for review. If it is a perf type PR, perf data is suggested to give.
-->
en: When sending a HTTP request, hertz will always send a Content-Type header, if the value was not specified by the user, it will default to application/x-www-form-urlencoded. This could cause undesirable effects on the server-side. This PR adds the SetNoDefaultContentType method to RequestHeader which when set to true, will omit the Content-Type header entirely if no Content-Type was specified.
zh(optional):

#### Which issue(s) this PR fixes:
<!--
Automatically closes linked issue when PR is merged.
Eg: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
close #360 
https://github.com/cloudwego/hertz/issues/359

Patch:
diff --git a/README.md b/README.md
index 01f7b5a..554319c 100644
--- a/README.md
+++ b/README.md
@@ -87,6 +87,10 @@ Hertz [həːts] is a high-usability, high-performance and high-extensibility Gol
 
 ## Blogs
 - [ByteDance Practice on Go Network Library](https://www.cloudwego.io/blog/2021/10/09/bytedance-practices-on-go-network-library/)
+- [Ultra-large-scale Enterprise-level Microservice HTTP Framework — Hertz is Officially Open Source!](https://www.cloudwego.io/zh/blog/2022/06/21/%E8%B6%85%E5%A4%A7%E8%A7%84%E6%A8%A1%E7%9A%84%E4%BC%81%E4%B8%9A%E7%BA%A7%E5%BE%AE%E6%9C%8D%E5%8A%A1-http-%E6%A1%86%E6%9E%B6-hertz-%E6%AD%A3%E5%BC%8F%E5%BC%80%E6%BA%90/)
+- [ByteDance Open Source Go HTTP Framework Hertz Design Practice](https://www.cloudwego.io/zh/blog/2022/06/21/%E5%AD%97%E8%8A%82%E8%B7%B3%E5%8A%A8%E5%BC%80%E6%BA%90-go-http-%E6%A1%86%E6%9E%B6-hertz-%E8%AE%BE%E8%AE%A1%E5%AE%9E%E8%B7%B5/)
+- [Help ByteDance Reduce Costs and Increase Efficiency, the Design Practice for Large-scale Enterprise-level HTTP Framework Hertz](https://www.cloudwego.io/zh/blog/2022/09/27/%E5%8A%A9%E5%8A%9B%E5%AD%97%E8%8A%82%E9%99%8D%E6%9C%AC%E5%A2%9E%E6%95%88%E5%A4%A7%E8%A7%84%E6%A8%A1%E4%BC%81%E4%B8%9A%E7%BA%A7-http-%E6%A1%86%E6%9E%B6-hertz-%E8%AE%BE%E8%AE%A1%E5%AE%9E%E8%B7%B5/)
+- [A Practical Introduction to the HTTP Framework Hertz: A Guide to Performance Testing](https://www.cloudwego.io/zh/blog/2022/11/01/http-%E6%A1%86%E6%9E%B6-hertz-%E5%AE%9E%E8%B7%B5%E5%85%A5%E9%97%A8%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95%E6%8C%87%E5%8D%97/)
 ## Contributing
 
 [Contributing](https://github.com/cloudwego/hertz/blob/main/CONTRIBUTING.md)
diff --git a/README_cn.md b/README_cn.md
index d06866b..942b743 100644
--- a/README_cn.md
+++ b/README_cn.md
@@ -87,6 +87,10 @@ Hertz[həːts] 是一个 Golang 微服务 HTTP 框架，在设计之初参考了
 
 ## 相关文章
 - [字节跳动在 Go 网络库上的实践](https://www.cloudwego.io/blog/2021/10/09/bytedance-practices-on-go-network-library/)
+- [超大规模的企业级微服务 HTTP 框架 — Hertz 正式开源！](https://www.cloudwego.io/zh/blog/2022/06/21/%E8%B6%85%E5%A4%A7%E8%A7%84%E6%A8%A1%E7%9A%84%E4%BC%81%E4%B8%9A%E7%BA%A7%E5%BE%AE%E6%9C%8D%E5%8A%A1-http-%E6%A1%86%E6%9E%B6-hertz-%E6%AD%A3%E5%BC%8F%E5%BC%80%E6%BA%90/)
+- [字节跳动开源 Go HTTP 框架 Hertz 设计实践](https://www.cloudwego.io/zh/blog/2022/06/21/%E5%AD%97%E8%8A%82%E8%B7%B3%E5%8A%A8%E5%BC%80%E6%BA%90-go-http-%E6%A1%86%E6%9E%B6-hertz-%E8%AE%BE%E8%AE%A1%E5%AE%9E%E8%B7%B5/)
+- [助力字节降本增效，大规模企业级 HTTP 框架 Hertz 设计实践](https://www.cloudwego.io/zh/blog/2022/09/27/%E5%8A%A9%E5%8A%9B%E5%AD%97%E8%8A%82%E9%99%8D%E6%9C%AC%E5%A2%9E%E6%95%88%E5%A4%A7%E8%A7%84%E6%A8%A1%E4%BC%81%E4%B8%9A%E7%BA%A7-http-%E6%A1%86%E6%9E%B6-hertz-%E8%AE%BE%E8%AE%A1%E5%AE%9E%E8%B7%B5/)
+- [HTTP 框架 Hertz 实践入门：性能测试指南](https://www.cloudwego.io/zh/blog/2022/11/01/http-%E6%A1%86%E6%9E%B6-hertz-%E5%AE%9E%E8%B7%B5%E5%85%A5%E9%97%A8%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95%E6%8C%87%E5%8D%97/)
 ## 贡献代码
   [Contributing](https://github.com/cloudwego/hertz/blob/main/CONTRIBUTING.md)
 ## RoadMap
diff --git a/cmd/hz/internal/protobuf/tags.go b/cmd/hz/internal/protobuf/tags.go
index c2b2b48..148689c 100644
--- a/cmd/hz/internal/protobuf/tags.go
+++ b/cmd/hz/internal/protobuf/tags.go
@@ -258,7 +258,7 @@ func m2s(mt model.Tag) (ret [2]string) {
 
 func reflectJsonTag(f protoreflect.FieldDescriptor) (ret model.Tag) {
 	ret.Key = "json"
-	ret.Value = checkSnakeName(f.JSONName())
+	ret.Value = checkSnakeName(f.Name())
 	if v := checkFirstOption(api.E_Body, f.Options()); v != nil {
 		ret.Value += ",string"
 	}
diff --git a/pkg/app/context.go b/pkg/app/context.go
index 5e15f9b..16797d7 100644
--- a/pkg/app/context.go
+++ b/pkg/app/context.go
@@ -727,6 +727,14 @@ func (ctx *RequestContext) GetInt(key string) (i int) {
 	return
 }
 
+// GetInt32 returns the value associated with the key as an integer. Return int32(0) when type is error.
+func (ctx *RequestContext) GetInt32(key string) (i32 int32) {
+	if val, ok := ctx.Get(key); ok && val != nil {
+		i32, _ = val.(int32)
+	}
+	return
+}
+
 // GetInt64 returns the value associated with the key as an integer. Return int64(0) when type is error.
 func (ctx *RequestContext) GetInt64(key string) (i64 int64) {
 	if val, ok := ctx.Get(key); ok && val != nil {
@@ -735,6 +743,38 @@ func (ctx *RequestContext) GetInt64(key string) (i64 int64) {
 	return
 }
 
+// GetUint returns the value associated with the key as an unsigned integer. Return uint(0) when type is error.
+func (ctx *RequestContext) GetUint(key string) (ui uint) {
+	if val, ok := ctx.Get(key); ok && val != nil {
+		ui, _ = val.(uint)
+	}
+	return
+}
+
+// GetUint32 returns the value associated with the key as an unsigned integer. Return uint32(0) when type is error.
+func (ctx *RequestContext) GetUint32(key string) (ui32 uint32) {
+	if val, ok := ctx.Get(key); ok && val != nil {
+		ui32, _ = val.(uint32)
+	}
+	return
+}
+
+// GetUint64 returns the value associated with the key as an unsigned integer. Return uint64(0) when type is error.
+func (ctx *RequestContext) GetUint64(key string) (ui64 uint64) {
+	if val, ok := ctx.Get(key); ok && val != nil {
+		ui64, _ = val.(uint64)
+	}
+	return
+}
+
+// GetFloat32 returns the value associated with the key as a float32. Return float32(0.0) when type is error.
+func (ctx *RequestContext) GetFloat32(key string) (f32 float32) {
+	if val, ok := ctx.Get(key); ok && val != nil {
+		f32, _ = val.(float32)
+	}
+	return
+}
+
 // GetFloat64 returns the value associated with the key as a float64. Return 0.0 when type is error.
 func (ctx *RequestContext) GetFloat64(key string) (f64 float64) {
 	if val, ok := ctx.Get(key); ok && val != nil {
diff --git a/pkg/protocol/header.go b/pkg/protocol/header.go
index eb28ade..a01966a 100644
--- a/pkg/protocol/header.go
+++ b/pkg/protocol/header.go
@@ -65,10 +65,11 @@ var (
 type RequestHeader struct {
 	noCopy nocopy.NoCopy //lint:ignore U1000 until noCopy is used
 
-	disableNormalizing bool
-	noHTTP11           bool
-	protocol           string
-	connectionClose    bool
+	disableNormalizing   bool
+	noHTTP11             bool
+	protocol             string
+	connectionClose      bool
+	noDefaultContentType bool
 
 	// These two fields have been moved close to other bool fields
 	// for reducing RequestHeader object size.
@@ -369,7 +370,7 @@ func (h *RequestHeader) AppendBytes(dst []byte) []byte {
 	}
 
 	contentType := h.ContentType()
-	if len(contentType) == 0 && !h.IgnoreBody() {
+	if len(contentType) == 0 && !h.IgnoreBody() && !h.noDefaultContentType {
 		contentType = bytestr.StrPostArgsContentType
 	}
 	if len(contentType) > 0 {
@@ -934,6 +935,7 @@ func (h *RequestHeader) CopyTo(dst *RequestHeader) {
 	dst.disableNormalizing = h.disableNormalizing
 	dst.noHTTP11 = h.noHTTP11
 	dst.connectionClose = h.connectionClose
+	dst.noDefaultContentType = h.noDefaultContentType
 
 	dst.contentLength = h.contentLength
 	dst.contentLengthBytes = append(dst.contentLengthBytes[:0], h.contentLengthBytes...)
@@ -990,6 +992,14 @@ func (h *RequestHeader) ContentType() []byte {
 	return h.contentType
 }
 
+// SetNoDefaultContentType controls the default Content-Type header behaviour.
+//
+// When set to false, the Content-Type header is sent with a default value if no Content-Type value is specified.
+// When set to true, no Content-Type header is sent if no Content-Type value is specified.
+func (h *RequestHeader) SetNoDefaultContentType(b bool) {
+	h.noDefaultContentType = b
+}
+
 // SetContentLength sets Content-Length header value.
 //
 // Negative content-length sets 'Transfer-Encoding: chunked' header.
@@ -1231,6 +1241,7 @@ func (h *RequestHeader) ResetSkipNormalize() {
 	h.noHTTP11 = false
 	h.connectionClose = false
 	h.protocol = ""
+	h.noDefaultContentType = false
 
 	h.contentLength = 0
 	h.contentLengthBytes = h.contentLengthBytes[:0]
diff --git a/pkg/protocol/http1/server.go b/pkg/protocol/http1/server.go
index b4e9b4e..8df3320 100644
--- a/pkg/protocol/http1/server.go
+++ b/pkg/protocol/http1/server.go
@@ -330,7 +330,8 @@ func (s Server) Serve(c context.Context, conn network.Conn) (err error) {
 		if connectionClose {
 			return errShortConnection
 		}
-
+		// Back to network layer to trigger.
+		// For now, only netpoll network mode has this feature.
 		if s.IdleTimeout == 0 {
 			return
 		}
diff --git a/pkg/route/engine.go b/pkg/route/engine.go
index d1fa73b..94d6d22 100644
--- a/pkg/route/engine.go
+++ b/pkg/route/engine.go
@@ -954,7 +954,7 @@ func iterate(method string, routes RoutesInfo, root *node) RoutesInfo {
 
 // for built-in http1 impl only.
 func newHttp1OptionFromEngine(engine *Engine) *http1.Option {
-	return &http1.Option{
+	opt := &http1.Option{
 		StreamRequestBody:            engine.options.StreamRequestBody,
 		GetOnly:                      engine.options.GetOnly,
 		DisablePreParseMultipartForm: engine.options.DisablePreParseMultipartForm,
@@ -970,4 +970,10 @@ func newHttp1OptionFromEngine(engine *Engine) *http1.Option {
 		EnableTrace:                  engine.IsTraceEnable(),
 		HijackConnHandle:             engine.HijackConnHandle,
 	}
+	// Idle timeout of standard network must not be zero. Set it to -1 seconds if it is zero.
+	// Due to the different triggering ways of the network library, see the actual use of this value for the detailed reasons.
+	if opt.IdleTimeout == 0 && engine.GetTransporterName() == "standard" {
+		opt.IdleTimeout = -1
+	}
+	return opt
 }


Test patch:
diff --git a/pkg/app/context_test.go b/pkg/app/context_test.go
index 138852a..2e55eeb 100644
--- a/pkg/app/context_test.go
+++ b/pkg/app/context_test.go
@@ -741,7 +741,8 @@ func TestContextSetGetValues(t *testing.T) {
 	c.Set("string", "this is a string")
 	c.Set("int32", int32(-42))
 	c.Set("int64", int64(42424242424242))
-	c.Set("uint64", uint64(42))
+	c.Set("uint32", uint32(42))
+	c.Set("uint64", uint64(42424242424242))
 	c.Set("float32", float32(4.2))
 	c.Set("float64", 4.2)
 	var a interface{} = 1
@@ -750,7 +751,8 @@ func TestContextSetGetValues(t *testing.T) {
 	assert.DeepEqual(t, c.MustGet("string").(string), "this is a string")
 	assert.DeepEqual(t, c.MustGet("int32").(int32), int32(-42))
 	assert.DeepEqual(t, c.MustGet("int64").(int64), int64(42424242424242))
-	assert.DeepEqual(t, c.MustGet("uint64").(uint64), uint64(42))
+	assert.DeepEqual(t, c.MustGet("uint32").(uint32), uint32(42))
+	assert.DeepEqual(t, c.MustGet("uint64").(uint64), uint64(42424242424242))
 	assert.DeepEqual(t, c.MustGet("float32").(float32), float32(4.2))
 	assert.DeepEqual(t, c.MustGet("float64").(float64), 4.2)
 	assert.DeepEqual(t, c.MustGet("intInterface").(int), 1)
@@ -780,6 +782,14 @@ func TestContextGetInt(t *testing.T) {
 	assert.DeepEqual(t, 0, c.GetInt("string"))
 }
 
+func TestContextGetInt32(t *testing.T) {
+	c := &RequestContext{}
+	c.Set("int32", int32(-42))
+	assert.DeepEqual(t, int32(-42), c.GetInt32("int32"))
+	c.Set("string", "this is a string")
+	assert.DeepEqual(t, int32(0), c.GetInt32("string"))
+}
+
 func TestContextGetInt64(t *testing.T) {
 	c := &RequestContext{}
 	c.Set("int64", int64(42424242424242))
@@ -788,6 +798,38 @@ func TestContextGetInt64(t *testing.T) {
 	assert.DeepEqual(t, int64(0), c.GetInt64("string"))
 }
 
+func TestContextGetUint(t *testing.T) {
+	c := &RequestContext{}
+	c.Set("uint", uint(1))
+	assert.DeepEqual(t, uint(1), c.GetUint("uint"))
+	c.Set("string", "this is a string")
+	assert.DeepEqual(t, uint(0), c.GetUint("string"))
+}
+
+func TestContextGetUint32(t *testing.T) {
+	c := &RequestContext{}
+	c.Set("uint32", uint32(42))
+	assert.DeepEqual(t, uint32(42), c.GetUint32("uint32"))
+	c.Set("string", "this is a string")
+	assert.DeepEqual(t, uint32(0), c.GetUint32("string"))
+}
+
+func TestContextGetUint64(t *testing.T) {
+	c := &RequestContext{}
+	c.Set("uint64", uint64(42424242424242))
+	assert.DeepEqual(t, uint64(42424242424242), c.GetUint64("uint64"))
+	c.Set("string", "this is a string")
+	assert.DeepEqual(t, uint64(0), c.GetUint64("string"))
+}
+
+func TestContextGetFloat32(t *testing.T) {
+	c := &RequestContext{}
+	c.Set("float32", float32(4.2))
+	assert.DeepEqual(t, float32(4.2), c.GetFloat32("float32"))
+	c.Set("string", "this is a string")
+	assert.DeepEqual(t, float32(0.0), c.GetFloat32("string"))
+}
+
 func TestContextGetFloat64(t *testing.T) {
 	c := &RequestContext{}
 	c.Set("float64", 4.2)
diff --git a/pkg/app/server/option_test.go b/pkg/app/server/option_test.go
index c68e2c3..5c23fec 100644
--- a/pkg/app/server/option_test.go
+++ b/pkg/app/server/option_test.go
@@ -35,6 +35,7 @@ func TestOptions(t *testing.T) {
 	}
 	opt := config.NewOptions([]config.Option{
 		WithReadTimeout(time.Second),
+		WithWriteTimeout(time.Second),
 		WithIdleTimeout(time.Second),
 		WithKeepAliveTimeout(time.Second),
 		WithRedirectTrailingSlash(false),
@@ -62,6 +63,7 @@ func TestOptions(t *testing.T) {
 		WithAutoReloadRender(true, 5*time.Second),
 	})
 	assert.DeepEqual(t, opt.ReadTimeout, time.Second)
+	assert.DeepEqual(t, opt.WriteTimeout, time.Second)
 	assert.DeepEqual(t, opt.IdleTimeout, time.Second)
 	assert.DeepEqual(t, opt.KeepAliveTimeout, time.Second)
 	assert.DeepEqual(t, opt.RedirectTrailingSlash, false)
diff --git a/pkg/common/test/mock/network.go b/pkg/common/test/mock/network.go
index c7111b2..bb08cb4 100644
--- a/pkg/common/test/mock/network.go
+++ b/pkg/common/test/mock/network.go
@@ -75,6 +75,10 @@ func (m *Conn) Release() error {
 func (m *Conn) Peek(i int) ([]byte, error) {
 	b, err := m.zr.Peek(i)
 	if err != nil || len(b) != i {
+		if m.readTimeout <= 0 {
+			// simulate timeout forever
+			select {}
+		}
 		time.Sleep(m.readTimeout)
 		return nil, errs.ErrTimeout
 	}
diff --git a/pkg/protocol/header_test.go b/pkg/protocol/header_test.go
index b534841..10db903 100644
--- a/pkg/protocol/header_test.go
+++ b/pkg/protocol/header_test.go
@@ -44,6 +44,7 @@ package protocol
 import (
 	"bytes"
 	"fmt"
+	"net/http"
 	"strings"
 	"testing"
 
@@ -518,3 +519,13 @@ func TestRequestHeaderDelAllCookies(t *testing.T) {
 		t.Fatalf("non-zero value: %q", hv)
 	}
 }
+
+func TestRequestHeaderSetNoDefaultContentType(t *testing.T) {
+	var h RequestHeader
+	h.SetMethod(http.MethodPost)
+	b := h.AppendBytes(nil)
+	assert.DeepEqual(t, b, []byte("POST / HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\n"))
+	h.SetNoDefaultContentType(true)
+	b = h.AppendBytes(nil)
+	assert.DeepEqual(t, b, []byte("POST / HTTP/1.1\r\n\r\n"))
+}
diff --git a/pkg/route/engine_test.go b/pkg/route/engine_test.go
index 1d53194..379b53f 100644
--- a/pkg/route/engine_test.go
+++ b/pkg/route/engine_test.go
@@ -257,6 +257,39 @@ func TestIdleTimeout01(t *testing.T) {
 	}
 }
 
+func TestIdleTimeout03(t *testing.T) {
+	engine := NewEngine(config.NewOptions(nil))
+	engine.options.IdleTimeout = 0
+	engine.transport = standard.NewTransporter(engine.options)
+	atomic.StoreUint32(&engine.status, statusRunning)
+	engine.Init()
+	atomic.StoreUint32(&engine.status, statusRunning)
+	engine.GET("/foo", func(c context.Context, ctx *app.RequestContext) {
+		time.Sleep(50 * time.Millisecond)
+		ctx.String(200, "ok")
+	})
+
+	conn := mock.NewConn("GET /foo HTTP/1.1\r\nHost: google.com\r\n\r\n" +
+		"GET /foo HTTP/1.1\r\nHost: google.com\r\nConnection: close\r\n\r\n")
+
+	ch := make(chan error)
+	startCh := make(chan error)
+	go func() {
+		<-startCh
+		ch <- engine.Serve(context.Background(), conn)
+	}()
+	close(startCh)
+	select {
+	case err := <-ch:
+		if !errors.Is(err, errs.ErrShortConnection) {
+			t.Errorf("err should be ErrShortConnection, but got %s", err)
+		}
+		return
+	case <-time.Tick(120 * time.Millisecond):
+		t.Errorf("timeout! should have been finished in 120ms...")
+	}
+}
+
 func TestEngine_Routes(t *testing.T) {
 	engine := NewEngine(config.NewOptions(nil))
 	engine.GET("/", handlerTest1)