Repository: go-kratos/kratos (go)

Issue:
[Feature] Add Unwrap to custom response writer.
Please see the FAQ in our main README.md before submitting your issue.

<!--
In order to accurately distinguish that the needs put forward by users are the needs of most users and reasonable needs, solicit community opinions through the process, and the features adopted by the community will be realized as new functions.

In order to make the proposal process as simple as possible, the process includes three stages: feature request - > proposal - > pull-request, where feature, proposal is issue and pull-request is the specific function implementation.

### Feature-request

In order to help the community correctly understand the requirements of the feature, the feature request issue needs to describe the functional requirements and relevant references or documents in detail. And the feature request issue can contain the basic description of the function, which can be used as a reference for the function implementation in the proposal.

### Proposal

Proposal contains the basic implementation methods of functions, such as interface definition, general usage of functions, etc.

### Pull-request

After the function is realized, a merge request will be initiated to associate the proposal issue with the function issue. After the merger is completed, all questions will be closed and the process will end.

### Decision process

When more than five maintainer members agree to implement the feature, a proposal issue will be created for detailed design. The status of the proposal is divided into: under discussion, finalized and abandoned. After reaching the final status, start specific implementation (PR can also be implemented synchronously during the discussion)

### Final decision maker mechanism

If the maintainer team members have major differences on a requirement, the final decision is made by @Terry Mao.
-->

### What problem is the feature used to solve?

Go added a new feature called [NewResponseController](https://pkg.go.dev/net/http#NewResponseController) in go 1.20. Pay attention to the doc blow:

> The ResponseWriter should be the original value passed to the [Handler.ServeHTTP] method, or have an Unwrap method returning the original ResponseWriter.

As it said, custom ResponseWriter should add an Unwrap method to return underlaying ResponseWriter.

Currently, there is a custom ResponseWriter in transport/http. And it is used in http.Context:

https://github.com/go-kratos/kratos/blob/311016862b20f14f9306f847b2d6bbb8c4f8b3c4/transport/http/context.go#L45-L66

Shall we support this feature, so that we can use some code likes below:

```go
_ = http.NewServer(
	http.ResponseEncoder(func(w http.ResponseWriter, r *http.Request, i any) error {
		rc := nt.NewResponseController(w)
		bs, _ := encoding.GetCodec("json").Marshal(i)

		n := len(bs) / 2

		n, _ = w.Write(bs[:n])    // 1. write half of the response
		_ = rc.Flush()            // 2. flush response first
		_, err := w.Write(bs[n:]) // 3. write the rest
		return err
	}),
)
```

### Requirements description of the feature

Add an "Unwrap" method to transport/http.responseWriter.

```go
func (w *responseWriter) Unwrap() http.ResponseWriter { return w.w }
```

### References

- [go doc NewResponseController](https://pkg.go.dev/net/http#NewResponseController)

Pull request description:
feat(transport/http): add unwrap method for returning underlying response writer
<!--
🎉 Thanks for sending a pull request to Kratos! Here are some tips for you:

1. If this is your first time contributing to Kratos, please read our contribution guide: https://go-kratos.dev/en/docs/community/contribution/
2. Ensure you have added or ran the appropriate tests and lint for your PR, please use `make lint` and `make test` before filing your PR, use `make clean` to tidy your go mod.
3. If the PR is unfinished, you may need to mark it as a WIP(Work In Progress) PR or Draft PR
4. Please use a semantic commits format title, such as `<type>[optional scope]: <description>`, see: https://go-kratos.dev/docs/community/contribution#type
5. at the same time, please note that similar work should be submitted in one PR as far as possible to reduce the workload of reviewers. Do not split a work into multiple PR unless it should.
-->

<!--
🎉 感谢您向 Kratos 发送 PR！以下是一些提示：
如果这是你第一次为 Kratos 贡献，请阅读我们的贡献指南：https://go-kratos.dev/en/docs/community/contribution/
2、确保您已经为您的 PR 添加或运行了适当的测试和lint，请在提交PR之前使用“make lint”和“make test”，使用“make clean”整理您的 go.mod。
3、如果 PR 未完成，您可能需要将其标记为 WIP（Work In Progress）PR 或 Draft PR
4、请使用语义提交格式标题，如“<类型>[可选范围]：<说明>`，请参阅：https://go-kratos.dev/docs/community/contribution#type
5. 同时请注意，同类的工作请尽量在一个PR中提交，以减轻 review 者的工作负担，不要把一项工作拆分成很多个PR，除非它应该这样做。
-->


#### Description (what this PR does / why we need it):
<!--
* The description should include the motivation for this PR or contrast this with previous behavior
-->

Add an `Unwrap` method for returning underlying response writer.

#### Which issue(s) this PR fixes (resolves / be part of):
<!--
* Automatically closes linked issue when PR is merged.
* If your PR is not fully resolved the issue, please use `part of #<issue number>` instead.

Usage: `fixes/resolves #<issue number>`, or `fixes/resolves (paste link of issue)`.
-->

resolves #3253

#### Other special notes for the reviewers:
<!--
* Somethings that need extra attention for the reviewers
* Some additional notes, TODO list, etc.
-->


Patch:
diff --git a/transport/http/context.go b/transport/http/context.go
index ab9a1d39..43ea78ce 100644
--- a/transport/http/context.go
+++ b/transport/http/context.go
@@ -57,6 +57,7 @@ func (w *responseWriter) Write(data []byte) (int, error) {
 	w.w.WriteHeader(w.code)
 	return w.w.Write(data)
 }
+func (w *responseWriter) Unwrap() http.ResponseWriter { return w.w }
 
 type wrapper struct {
 	router *Router


Test patch:
diff --git a/transport/http/context_test.go b/transport/http/context_test.go
index 6cae1b9a..8cc79da0 100644
--- a/transport/http/context_test.go
+++ b/transport/http/context_test.go
@@ -100,6 +100,34 @@ func TestContextResponse(t *testing.T) {
 	}
 }
 
+func TestResponseUnwrap(t *testing.T) {
+	res := httptest.NewRecorder()
+	f := func(rw http.ResponseWriter, r *http.Request, a interface{}) error {
+		u, ok := rw.(interface {
+			Unwrap() http.ResponseWriter
+		})
+		if !ok {
+			return errors.New("can not unwrap")
+		}
+		w := u.Unwrap()
+		if !reflect.DeepEqual(w, res) {
+			return errors.New("underlying response writer not equal")
+		}
+		return nil
+	}
+
+	w := wrapper{
+		router: &Router{srv: &Server{enc: f}},
+		req:    nil,
+		res:    res,
+		w:      responseWriter{200, res},
+	}
+	err := w.Result(200, "ok")
+	if err != nil {
+		t.Errorf("expected %v, got %v", nil, err)
+	}
+}
+
 func TestContextBindQuery(t *testing.T) {
 	w := wrapper{
 		router: testRouter,