Repository: gin-gonic/gin
Pull request #3851 (2024-02-21 19:01:42)

chore(header): Add support for RFC 9512: application/yaml
[RFC 9512](https://www.rfc-editor.org/rfc/rfc9512.html) deprecates `application/x-yaml` in favor of `application/yaml`. This PR changes that in Gin, with two commits:

1. accepts `application/yaml` for bindings
2. uses `application/yaml` when rendering

The second one may be considered as a breaking change, hence the separate commit.


Patch:
diff --git a/binding/binding.go b/binding/binding.go
index 4094852..036b329 100644
--- a/binding/binding.go
+++ b/binding/binding.go
@@ -21,6 +21,7 @@ const (
 	MIMEMSGPACK           = "application/x-msgpack"
 	MIMEMSGPACK2          = "application/msgpack"
 	MIMEYAML              = "application/x-yaml"
+	MIMEYAML2             = "application/yaml"
 	MIMETOML              = "application/toml"
 )
 
@@ -102,7 +103,7 @@ func Default(method, contentType string) Binding {
 		return ProtoBuf
 	case MIMEMSGPACK, MIMEMSGPACK2:
 		return MsgPack
-	case MIMEYAML:
+	case MIMEYAML, MIMEYAML2:
 		return YAML
 	case MIMETOML:
 		return TOML
diff --git a/binding/binding_nomsgpack.go b/binding/binding_nomsgpack.go
index 93ad8ba..552a86b 100644
--- a/binding/binding_nomsgpack.go
+++ b/binding/binding_nomsgpack.go
@@ -19,6 +19,7 @@ const (
 	MIMEMultipartPOSTForm = "multipart/form-data"
 	MIMEPROTOBUF          = "application/x-protobuf"
 	MIMEYAML              = "application/x-yaml"
+	MIMEYAML2             = "application/yaml"
 	MIMETOML              = "application/toml"
 )
 
@@ -96,7 +97,7 @@ func Default(method, contentType string) Binding {
 		return XML
 	case MIMEPROTOBUF:
 		return ProtoBuf
-	case MIMEYAML:
+	case MIMEYAML, MIMEYAML2:
 		return YAML
 	case MIMEMultipartPOSTForm:
 		return FormMultipart
diff --git a/context.go b/context.go
index 420ff16..126d35d 100644
--- a/context.go
+++ b/context.go
@@ -113,20 +113,24 @@ func (c *Context) Copy() *Context {
 	cp := Context{
 		writermem: c.writermem,
 		Request:   c.Request,
-		Params:    c.Params,
 		engine:    c.engine,
 	}
+
 	cp.writermem.ResponseWriter = nil
 	cp.Writer = &cp.writermem
 	cp.index = abortIndex
 	cp.handlers = nil
-	cp.Keys = map[string]any{}
-	for k, v := range c.Keys {
+
+	cKeys := c.Keys
+	cp.Keys = make(map[string]any, len(cKeys))
+	for k, v := range cKeys {
 		cp.Keys[k] = v
 	}
-	paramCopy := make([]Param, len(cp.Params))
-	copy(paramCopy, cp.Params)
-	cp.Params = paramCopy
+
+	cParams := c.Params
+	cp.Params = make([]Param, len(cParams))
+	copy(cp.Params, cParams)
+
 	return &cp
 }
 
diff --git a/render/yaml.go b/render/yaml.go
index fc927c1..042bb82 100644
--- a/render/yaml.go
+++ b/render/yaml.go
@@ -15,7 +15,7 @@ type YAML struct {
 	Data any
 }
 
-var yamlContentType = []string{"application/x-yaml; charset=utf-8"}
+var yamlContentType = []string{"application/yaml; charset=utf-8"}
 
 // Render (YAML) marshals the given interface object and writes data with custom ContentType.
 func (r YAML) Render(w http.ResponseWriter) error {


Test patch:
diff --git a/binding/binding_test.go b/binding/binding_test.go
index 9af4f88..feb8eed 100644
--- a/binding/binding_test.go
+++ b/binding/binding_test.go
@@ -164,6 +164,8 @@ func TestBindingDefault(t *testing.T) {
 
 	assert.Equal(t, YAML, Default("POST", MIMEYAML))
 	assert.Equal(t, YAML, Default("PUT", MIMEYAML))
+	assert.Equal(t, YAML, Default("POST", MIMEYAML2))
+	assert.Equal(t, YAML, Default("PUT", MIMEYAML2))
 
 	assert.Equal(t, TOML, Default("POST", MIMETOML))
 	assert.Equal(t, TOML, Default("PUT", MIMETOML))
diff --git a/context_test.go b/context_test.go
index 88165c0..d060ccf 100644
--- a/context_test.go
+++ b/context_test.go
@@ -1060,7 +1060,7 @@ func TestContextRenderUTF8Attachment(t *testing.T) {
 }
 
 // TestContextRenderYAML tests that the response is serialized as YAML
-// and Content-Type is set to application/x-yaml
+// and Content-Type is set to application/yaml
 func TestContextRenderYAML(t *testing.T) {
 	w := httptest.NewRecorder()
 	c, _ := CreateTestContext(w)
@@ -1069,7 +1069,7 @@ func TestContextRenderYAML(t *testing.T) {
 
 	assert.Equal(t, http.StatusCreated, w.Code)
 	assert.Equal(t, "foo: bar\n", w.Body.String())
-	assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type"))
+	assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
 }
 
 // TestContextRenderTOML tests that the response is serialized as TOML
@@ -1217,7 +1217,7 @@ func TestContextNegotiationWithYAML(t *testing.T) {
 
 	assert.Equal(t, http.StatusOK, w.Code)
 	assert.Equal(t, "foo: bar\n", w.Body.String())
-	assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type"))
+	assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
 }
 
 func TestContextNegotiationWithTOML(t *testing.T) {
diff --git a/render/render_test.go b/render/render_test.go
index c9db635..145f131 100644
--- a/render/render_test.go
+++ b/render/render_test.go
@@ -280,12 +280,12 @@ b:
 	d: [3, 4]
 	`
 	(YAML{data}).WriteContentType(w)
-	assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type"))
+	assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
 
 	err := (YAML{data}).Render(w)
 	assert.NoError(t, err)
 	assert.Equal(t, "|4-\n    a : Easy!\n    b:\n    \tc: 2\n    \td: [3, 4]\n    \t\n", w.Body.String())
-	assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type"))
+	assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
 }
 
 type fail struct{}
diff --git a/routes_test.go b/routes_test.go
index a0ff695..185abd9 100644
--- a/routes_test.go
+++ b/routes_test.go
@@ -180,58 +180,58 @@ func TestRouteRedirectTrailingSlash(t *testing.T) {
 
 	w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/api"})
 	assert.Equal(t, "/api/path2/", w.Header().Get("Location"))
-	assert.Equal(t, 301, w.Code)
+	assert.Equal(t, http.StatusMovedPermanently, w.Code)
 
 	w = PerformRequest(router, http.MethodGet, "/path2/", header{Key: "X-Forwarded-Prefix", Value: "/api/"})
-	assert.Equal(t, 200, w.Code)
+	assert.Equal(t, http.StatusOK, w.Code)
 
 	w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "../../api#?"})
 	assert.Equal(t, "/api/path", w.Header().Get("Location"))
-	assert.Equal(t, 301, w.Code)
+	assert.Equal(t, http.StatusMovedPermanently, w.Code)
 
 	w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "../../api"})
 	assert.Equal(t, "/api/path", w.Header().Get("Location"))
-	assert.Equal(t, 301, w.Code)
+	assert.Equal(t, http.StatusMovedPermanently, w.Code)
 
 	w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "../../api"})
 	assert.Equal(t, "/api/path2/", w.Header().Get("Location"))
-	assert.Equal(t, 301, w.Code)
+	assert.Equal(t, http.StatusMovedPermanently, w.Code)
 
 	w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/../../api"})
 	assert.Equal(t, "/api/path2/", w.Header().Get("Location"))
-	assert.Equal(t, 301, w.Code)
+	assert.Equal(t, http.StatusMovedPermanently, w.Code)
 
 	w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "api/../../"})
 	assert.Equal(t, "//path", w.Header().Get("Location"))
-	assert.Equal(t, 301, w.Code)
+	assert.Equal(t, http.StatusMovedPermanently, w.Code)
 
 	w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "api/../../../"})
 	assert.Equal(t, "/path", w.Header().Get("Location"))
-	assert.Equal(t, 301, w.Code)
+	assert.Equal(t, http.StatusMovedPermanently, w.Code)
 
 	w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "../../gin-gonic.com"})
 	assert.Equal(t, "/gin-goniccom/path2/", w.Header().Get("Location"))
-	assert.Equal(t, 301, w.Code)
+	assert.Equal(t, http.StatusMovedPermanently, w.Code)
 
 	w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/../../gin-gonic.com"})
 	assert.Equal(t, "/gin-goniccom/path2/", w.Header().Get("Location"))
-	assert.Equal(t, 301, w.Code)
+	assert.Equal(t, http.StatusMovedPermanently, w.Code)
 
 	w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "https://gin-gonic.com/#"})
 	assert.Equal(t, "https/gin-goniccom/https/gin-goniccom/path", w.Header().Get("Location"))
-	assert.Equal(t, 301, w.Code)
+	assert.Equal(t, http.StatusMovedPermanently, w.Code)
 
 	w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "#api"})
 	assert.Equal(t, "api/api/path", w.Header().Get("Location"))
-	assert.Equal(t, 301, w.Code)
+	assert.Equal(t, http.StatusMovedPermanently, w.Code)
 
 	w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "/nor-mal/#?a=1"})
 	assert.Equal(t, "/nor-mal/a1/path", w.Header().Get("Location"))
-	assert.Equal(t, 301, w.Code)
+	assert.Equal(t, http.StatusMovedPermanently, w.Code)
 
 	w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "/nor-mal/%2e%2e/"})
 	assert.Equal(t, "/nor-mal/2e2e/path", w.Header().Get("Location"))
-	assert.Equal(t, 301, w.Code)
+	assert.Equal(t, http.StatusMovedPermanently, w.Code)
 
 	router.RedirectTrailingSlash = false
 
@@ -619,11 +619,11 @@ func TestRouterNotFound(t *testing.T) {
 	router = New()
 	router.NoRoute(func(c *Context) {
 		if c.Request.RequestURI == "/login" {
-			c.String(200, "login")
+			c.String(http.StatusOK, "login")
 		}
 	})
 	router.GET("/logout", func(c *Context) {
-		c.String(200, "logout")
+		c.String(http.StatusOK, "logout")
 	})
 	w = PerformRequest(router, http.MethodGet, "/login")
 	assert.Equal(t, "login", w.Body.String())
@@ -635,7 +635,7 @@ func TestRouterStaticFSNotFound(t *testing.T) {
 	router := New()
 	router.StaticFS("/", http.FileSystem(http.Dir("/thisreallydoesntexist/")))
 	router.NoRoute(func(c *Context) {
-		c.String(404, "non existent")
+		c.String(http.StatusNotFound, "non existent")
 	})
 
 	w := PerformRequest(router, http.MethodGet, "/nonexistent")
@@ -718,12 +718,12 @@ func TestRouteRawPathNoUnescape(t *testing.T) {
 func TestRouteServeErrorWithWriteHeader(t *testing.T) {
 	route := New()
 	route.Use(func(c *Context) {
-		c.Status(421)
+		c.Status(http.StatusMisdirectedRequest)
 		c.Next()
 	})
 
 	w := PerformRequest(route, http.MethodGet, "/NotFound")
-	assert.Equal(t, 421, w.Code)
+	assert.Equal(t, http.StatusMisdirectedRequest, w.Code)
 	assert.Equal(t, 0, w.Body.Len())
 }