Repository: go-kratos/kratos
Pull request #1410 (2021-09-01 11:55:56)

feat: Support custom status code conversion from HTTP and gRPC.
<!--
🎉 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 for your PR, such as using `go test ./...`
3. If the PR is unfinished, you may need mark it as a WIP(Work In Progress) PR or draft PR
4. Please use a conventional commits format title: `<type>[optional scope]: <description>`
    
    Some suggestion on <type>:

    fix: A bug fix
    feat: A new feature
    deps: Changes external dependencies
    break: Changes has break change

    docs: Documentation only changes
    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, etc)
    test: Adding missing tests or correcting existing tests
    chore Daily work, examples, etc.
    ci: Changes to our CI configuration files and scripts
-->

#### Description (what this PR does / why we need it):
<!--
* The description should include the motivation for this PR or contrast this with previous behavior
-->

Support custom status code conversion from HTTP and gRPC.

#### Which issue(s) this PR fixes:
<!--
* Automatically closes linked issue when PR is merged.

Usage: `fixes/resolves #<issue number>`, or `fixes/resolves (paste link of issue)`.
-->
feature #1409

#### Other special notes for reviewer:


kagaya85: Is this better to make a new file named `utils.go` under `transport` and create new exported function/interface to invoke the `StatusConverter` in `internel/httputils` ?
letian0805: > Is this better to make a new file named `utils.go` under `transport` and create new exported function/interface to invoke the `StatusConverter` in `internel/httputils` ?

Move httputils outside internal not only for StatusConverter, also for other functions.
codecov-commenter: # [Codecov](https://codecov.io/gh/go-kratos/kratos/pull/1410?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) Report
> Merging [#1410](https://codecov.io/gh/go-kratos/kratos/pull/1410?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) (d1994c0) into [main](https://codecov.io/gh/go-kratos/kratos/commit/515b71ec9061d07f0adf6c3546b79e791ffc4ded?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) (515b71e) will **increase** coverage by `1.61%`.
> The diff coverage is `100.00%`.

> :exclamation: Current head d1994c0 differs from pull request most recent head bdfda42. Consider uploading reports for the commit bdfda42 to get more accurate results
[![Impacted file tree graph](https://codecov.io/gh/go-kratos/kratos/pull/1410/graphs/tree.svg?width=650&height=150&src=pr&token=XwnmBTz78x&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos)](https://codecov.io/gh/go-kratos/kratos/pull/1410?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos)

```diff
@@            Coverage Diff             @@
##             main    #1410      +/-   ##
==========================================
+ Coverage   63.19%   64.80%   +1.61%     
==========================================
  Files          65       63       -2     
  Lines        3089     2921     -168     
==========================================
- Hits         1952     1893      -59     
+ Misses        974      869     -105     
+ Partials      163      159       -4     
```


| [Impacted Files](https://codecov.io/gh/go-kratos/kratos/pull/1410?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) | Coverage Δ | |
|---|---|---|
| [errors/errors.go](https://codecov.io/gh/go-kratos/kratos/pull/1410/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos#diff-ZXJyb3JzL2Vycm9ycy5nbw==) | `80.76% <ø> (ø)` | |
| [transport/http/client.go](https://codecov.io/gh/go-kratos/kratos/pull/1410/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos#diff-dHJhbnNwb3J0L2h0dHAvY2xpZW50Lmdv) | `65.02% <ø> (ø)` | |
| [transport/http/codec.go](https://codecov.io/gh/go-kratos/kratos/pull/1410/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos#diff-dHJhbnNwb3J0L2h0dHAvY29kZWMuZ28=) | `63.88% <ø> (ø)` | |
| [httputil/http.go](https://codecov.io/gh/go-kratos/kratos/pull/1410/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos#diff-aHR0cHV0aWwvaHR0cC5nbw==) | `100.00% <100.00%> (ø)` | |
| [errors/errors.pb.go](https://codecov.io/gh/go-kratos/kratos/pull/1410/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos#diff-ZXJyb3JzL2Vycm9ycy5wYi5nbw==) | | |
| [transport/http/binding/test.pb.go](https://codecov.io/gh/go-kratos/kratos/pull/1410/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos#diff-dHJhbnNwb3J0L2h0dHAvYmluZGluZy90ZXN0LnBiLmdv) | | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/go-kratos/kratos/pull/1410?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/go-kratos/kratos/pull/1410?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos). Last update [515b71e...bdfda42](https://codecov.io/gh/go-kratos/kratos/pull/1410?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos).

longXboy: go linit fix一下呢
longXboy: please fix go lint warning
letian0805: > please fix go lint warning

done

Patch:
diff --git a/errors/errors.go b/errors/errors.go
index 351830cb..8b2e7f55 100644
--- a/errors/errors.go
+++ b/errors/errors.go
@@ -4,7 +4,8 @@ import (
 	"errors"
 	"fmt"
 
-	"github.com/go-kratos/kratos/v2/internal/httputil"
+	httpstatus "github.com/go-kratos/kratos/v2/transport/http/status"
+
 	"google.golang.org/genproto/googleapis/rpc/errdetails"
 	"google.golang.org/grpc/status"
 	"google.golang.org/protobuf/proto"
@@ -27,7 +28,7 @@ func (e *Error) Error() string {
 
 // GRPCStatus returns the Status represented by se.
 func (e *Error) GRPCStatus() *status.Status {
-	s, _ := status.New(httputil.GRPCCodeFromStatus(int(e.Code)), e.Message).
+	s, _ := status.New(httpstatus.ToGRPCCode(int(e.Code)), e.Message).
 		WithDetails(&errdetails.ErrorInfo{
 			Reason:   e.Reason,
 			Metadata: e.Metadata,
@@ -105,7 +106,7 @@ func FromError(err error) *Error {
 			switch d := detail.(type) {
 			case *errdetails.ErrorInfo:
 				return New(
-					httputil.StatusFromGRPCCode(gs.Code()),
+					httpstatus.FromGRPCCode(gs.Code()),
 					d.Reason,
 					gs.Message(),
 				).WithMetadata(d.Metadata)
diff --git a/examples/go.sum b/examples/go.sum
index a58ec036..9042089c 100644
--- a/examples/go.sum
+++ b/examples/go.sum
@@ -164,11 +164,11 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
 github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
+github.com/go-kratos/aegis v0.1.0/go.mod h1:jYeSQ3Gesba478zEnujOiG5QdsyF3Xk/8owFUeKcHxw=
 github.com/go-kratos/gin v0.1.0 h1:yq5GfZnSNo8cOIqxqPE0FVNQ8fm++oKQBd3/rTTp4oI=
 github.com/go-kratos/gin v0.1.0/go.mod h1:KzE88+mV1QJdmiT+sU2D560ucOXnWZx+4m0OnVcSyzQ=
 github.com/go-kratos/grpc-gateway/v2 v2.0.0-20210804092615-bdc92eb5ce83 h1:YN1KsqX3mB8hkSrOLZv3xZaZh+lZ/L7QC00RATFLJSg=
 github.com/go-kratos/grpc-gateway/v2 v2.0.0-20210804092615-bdc92eb5ce83/go.mod h1:qV5/s5A7wfY7GkNzptcUFRGAG4Y8H4mlXpSrOkDEIk0=
-github.com/go-kratos/sra v0.0.0-20210905131750-b5f23c31b44c/go.mod h1:AyUnb2L926UF9iG+ytoICDK8Ou9QOEhLu/J36P0WYu4=
 github.com/go-kratos/swagger-api v1.0.0 h1:R1JcAqLSvfDzqHoDnpK3Jt1MEJAXGNifV7AnBEhP3PE=
 github.com/go-kratos/swagger-api v1.0.0/go.mod h1:BPeS1OUN4jtMitb99ptZAagAgyQ51S2F2vd9dT/J3eM=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
diff --git a/go.mod b/go.mod
index 46dddb1e..cc429a27 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@ go 1.15
 
 require (
 	github.com/fsnotify/fsnotify v1.4.9
-	github.com/go-kratos/sra v0.0.0-20210905131750-b5f23c31b44c
+	github.com/go-kratos/aegis v0.1.0
 	github.com/go-playground/form/v4 v4.2.0
 	github.com/google/uuid v1.3.0
 	github.com/gorilla/mux v1.8.0
diff --git a/go.sum b/go.sum
index 7dc0ac20..0601ae32 100644
--- a/go.sum
+++ b/go.sum
@@ -20,8 +20,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
 github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/go-kratos/sra v0.0.0-20210905131750-b5f23c31b44c h1:vLjfP5Or+owmNTeZBkDjHIXBj2yPgH0fyiy888ytfLc=
-github.com/go-kratos/sra v0.0.0-20210905131750-b5f23c31b44c/go.mod h1:AyUnb2L926UF9iG+ytoICDK8Ou9QOEhLu/J36P0WYu4=
+github.com/go-kratos/aegis v0.1.0 h1:Ip1qlSOw/b4o3JOWXQrSgwkjQjuCsirShk6gmzCg+44=
+github.com/go-kratos/aegis v0.1.0/go.mod h1:jYeSQ3Gesba478zEnujOiG5QdsyF3Xk/8owFUeKcHxw=
 github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY=
 github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
 github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
diff --git a/internal/httputil/http.go b/internal/httputil/http.go
index 1d63c363..81adf6ba 100644
--- a/internal/httputil/http.go
+++ b/internal/httputil/http.go
@@ -1,19 +1,11 @@
 package httputil
 
 import (
-	"net/http"
 	"strings"
-
-	"google.golang.org/grpc/codes"
 )
 
 const (
 	baseContentType = "application"
-
-	// StatusClientClosed is non-standard http status code,
-	// which defined by nginx.
-	// https://httpstatus.in/499/
-	StatusClientClosed = 499
 )
 
 // ContentType returns the content-type with base prefix.
@@ -40,77 +32,3 @@ func ContentSubtype(contentType string) string {
 	}
 	return contentType[left+1 : right]
 }
-
-// GRPCCodeFromStatus converts a HTTP error code into the corresponding gRPC response status.
-// See: https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto
-func GRPCCodeFromStatus(code int) codes.Code {
-	switch code {
-	case http.StatusOK:
-		return codes.OK
-	case http.StatusBadRequest:
-		return codes.InvalidArgument
-	case http.StatusUnauthorized:
-		return codes.Unauthenticated
-	case http.StatusForbidden:
-		return codes.PermissionDenied
-	case http.StatusNotFound:
-		return codes.NotFound
-	case http.StatusConflict:
-		return codes.Aborted
-	case http.StatusTooManyRequests:
-		return codes.ResourceExhausted
-	case http.StatusInternalServerError:
-		return codes.Internal
-	case http.StatusNotImplemented:
-		return codes.Unimplemented
-	case http.StatusServiceUnavailable:
-		return codes.Unavailable
-	case http.StatusGatewayTimeout:
-		return codes.DeadlineExceeded
-	case StatusClientClosed:
-		return codes.Canceled
-	}
-	return codes.Unknown
-}
-
-// StatusFromGRPCCode converts a gRPC error code into the corresponding HTTP response status.
-// See: https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto
-func StatusFromGRPCCode(code codes.Code) int {
-	switch code {
-	case codes.OK:
-		return http.StatusOK
-	case codes.Canceled:
-		return StatusClientClosed
-	case codes.Unknown:
-		return http.StatusInternalServerError
-	case codes.InvalidArgument:
-		return http.StatusBadRequest
-	case codes.DeadlineExceeded:
-		return http.StatusGatewayTimeout
-	case codes.NotFound:
-		return http.StatusNotFound
-	case codes.AlreadyExists:
-		return http.StatusConflict
-	case codes.PermissionDenied:
-		return http.StatusForbidden
-	case codes.Unauthenticated:
-		return http.StatusUnauthorized
-	case codes.ResourceExhausted:
-		return http.StatusTooManyRequests
-	case codes.FailedPrecondition:
-		return http.StatusBadRequest
-	case codes.Aborted:
-		return http.StatusConflict
-	case codes.OutOfRange:
-		return http.StatusBadRequest
-	case codes.Unimplemented:
-		return http.StatusNotImplemented
-	case codes.Internal:
-		return http.StatusInternalServerError
-	case codes.Unavailable:
-		return http.StatusServiceUnavailable
-	case codes.DataLoss:
-		return http.StatusInternalServerError
-	}
-	return http.StatusInternalServerError
-}
diff --git a/middleware/circuitbreaker/circuitbreaker.go b/middleware/circuitbreaker/circuitbreaker.go
index 67991b45..c80d45ba 100644
--- a/middleware/circuitbreaker/circuitbreaker.go
+++ b/middleware/circuitbreaker/circuitbreaker.go
@@ -3,12 +3,13 @@ package circuitbreaker
 import (
 	"context"
 
+	"github.com/go-kratos/aegis/circuitbreaker"
+	"github.com/go-kratos/aegis/circuitbreaker/sre"
 	"github.com/go-kratos/kratos/v2/errors"
 	"github.com/go-kratos/kratos/v2/middleware"
-	"github.com/go-kratos/sra/circuitbreaker"
-	"github.com/go-kratos/sra/circuitbreaker/sre"
 )
 
+// Option is circuit breaker option.
 type Option func(*options)
 
 // WithBreaker set circuit breaker implentation
diff --git a/middleware/ratelimit/limiter.go b/middleware/ratelimit/limiter.go
index c71fe205..bcfbb4fc 100644
--- a/middleware/ratelimit/limiter.go
+++ b/middleware/ratelimit/limiter.go
@@ -3,12 +3,13 @@ package ratelimit
 import (
 	"context"
 
+	"github.com/go-kratos/aegis/ratelimit"
+	"github.com/go-kratos/aegis/ratelimit/bbr"
 	"github.com/go-kratos/kratos/v2/errors"
 	"github.com/go-kratos/kratos/v2/middleware"
-	"github.com/go-kratos/sra/ratelimit"
-	"github.com/go-kratos/sra/ratelimit/bbr"
 )
 
+// Option is ratelimit option.
 type Option func(*options)
 
 // WithLimiter set Limiter implementation,
diff --git a/transport/http/status/status.go b/transport/http/status/status.go
new file mode 100644
index 00000000..d2240ec7
--- /dev/null
+++ b/transport/http/status/status.go
@@ -0,0 +1,110 @@
+package status
+
+import (
+	"net/http"
+
+	"google.golang.org/grpc/codes"
+)
+
+const (
+	// ClientClosed is non-standard http status code,
+	// which defined by nginx.
+	// https://httpstatus.in/499/
+	ClientClosed = 499
+)
+
+type Converter interface {
+	// ToGRPCCode converts an HTTP error code into the corresponding gRPC response status.
+	ToGRPCCode(code int) codes.Code
+
+	// FromGRPCCode converts a gRPC error code into the corresponding HTTP response status.
+	FromGRPCCode(code codes.Code) int
+}
+
+type statusConverter struct{}
+
+var DefaultConverter Converter = statusConverter{}
+
+// ToGRPCCode converts a HTTP error code into the corresponding gRPC response status.
+// See: https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto
+func (c statusConverter) ToGRPCCode(code int) codes.Code {
+	switch code {
+	case http.StatusOK:
+		return codes.OK
+	case http.StatusBadRequest:
+		return codes.InvalidArgument
+	case http.StatusUnauthorized:
+		return codes.Unauthenticated
+	case http.StatusForbidden:
+		return codes.PermissionDenied
+	case http.StatusNotFound:
+		return codes.NotFound
+	case http.StatusConflict:
+		return codes.Aborted
+	case http.StatusTooManyRequests:
+		return codes.ResourceExhausted
+	case http.StatusInternalServerError:
+		return codes.Internal
+	case http.StatusNotImplemented:
+		return codes.Unimplemented
+	case http.StatusServiceUnavailable:
+		return codes.Unavailable
+	case http.StatusGatewayTimeout:
+		return codes.DeadlineExceeded
+	case ClientClosed:
+		return codes.Canceled
+	}
+	return codes.Unknown
+}
+
+// FromGRPCCode converts a gRPC error code into the corresponding HTTP response status.
+// See: https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto
+func (c statusConverter) FromGRPCCode(code codes.Code) int {
+	switch code {
+	case codes.OK:
+		return http.StatusOK
+	case codes.Canceled:
+		return ClientClosed
+	case codes.Unknown:
+		return http.StatusInternalServerError
+	case codes.InvalidArgument:
+		return http.StatusBadRequest
+	case codes.DeadlineExceeded:
+		return http.StatusGatewayTimeout
+	case codes.NotFound:
+		return http.StatusNotFound
+	case codes.AlreadyExists:
+		return http.StatusConflict
+	case codes.PermissionDenied:
+		return http.StatusForbidden
+	case codes.Unauthenticated:
+		return http.StatusUnauthorized
+	case codes.ResourceExhausted:
+		return http.StatusTooManyRequests
+	case codes.FailedPrecondition:
+		return http.StatusBadRequest
+	case codes.Aborted:
+		return http.StatusConflict
+	case codes.OutOfRange:
+		return http.StatusBadRequest
+	case codes.Unimplemented:
+		return http.StatusNotImplemented
+	case codes.Internal:
+		return http.StatusInternalServerError
+	case codes.Unavailable:
+		return http.StatusServiceUnavailable
+	case codes.DataLoss:
+		return http.StatusInternalServerError
+	}
+	return http.StatusInternalServerError
+}
+
+// ToGRPCCode converts an HTTP error code into the corresponding gRPC response status.
+func ToGRPCCode(code int) codes.Code {
+	return DefaultConverter.ToGRPCCode(code)
+}
+
+// FromGRPCCode converts a gRPC error code into the corresponding HTTP response status.
+func FromGRPCCode(code codes.Code) int {
+	return DefaultConverter.FromGRPCCode(code)
+}


Test patch:
diff --git a/internal/httputil/http_test.go b/internal/httputil/http_test.go
index fcf9a020..da252a96 100644
--- a/internal/httputil/http_test.go
+++ b/internal/httputil/http_test.go
@@ -1,10 +1,7 @@
 package httputil
 
 import (
-	"net/http"
 	"testing"
-
-	"google.golang.org/grpc/codes"
 )
 
 func TestContentSubtype(t *testing.T) {
@@ -31,69 +28,6 @@ func TestContentSubtype(t *testing.T) {
 	}
 }
 
-func TestGRPCCodeFromStatus(t *testing.T) {
-	tests := []struct {
-		name string
-		code int
-		want codes.Code
-	}{
-		{"http.StatusOK", http.StatusOK, codes.OK},
-		{"http.StatusBadRequest", http.StatusBadRequest, codes.InvalidArgument},
-		{"http.StatusUnauthorized", http.StatusUnauthorized, codes.Unauthenticated},
-		{"http.StatusForbidden", http.StatusForbidden, codes.PermissionDenied},
-		{"http.StatusNotFound", http.StatusNotFound, codes.NotFound},
-		{"http.StatusConflict", http.StatusConflict, codes.Aborted},
-		{"http.StatusTooManyRequests", http.StatusTooManyRequests, codes.ResourceExhausted},
-		{"http.StatusInternalServerError", http.StatusInternalServerError, codes.Internal},
-		{"http.StatusNotImplemented", http.StatusNotImplemented, codes.Unimplemented},
-		{"http.StatusServiceUnavailable", http.StatusServiceUnavailable, codes.Unavailable},
-		{"http.StatusGatewayTimeout", http.StatusGatewayTimeout, codes.DeadlineExceeded},
-		{"StatusClientClosed", StatusClientClosed, codes.Canceled},
-		{"else", 100000, codes.Unknown},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := GRPCCodeFromStatus(tt.code); got != tt.want {
-				t.Errorf("GRPCCodeFromStatus() = %v, want %v", got, tt.want)
-			}
-		})
-	}
-}
-
-func TestStatusFromGRPCCode(t *testing.T) {
-	tests := []struct {
-		name string
-		code codes.Code
-		want int
-	}{
-		{"codes.OK", codes.OK, http.StatusOK},
-		{"codes.Canceled", codes.Canceled, StatusClientClosed},
-		{"codes.Unknown", codes.Unknown, http.StatusInternalServerError},
-		{"codes.InvalidArgument", codes.InvalidArgument, http.StatusBadRequest},
-		{"codes.DeadlineExceeded", codes.DeadlineExceeded, http.StatusGatewayTimeout},
-		{"codes.NotFound", codes.NotFound, http.StatusNotFound},
-		{"codes.AlreadyExists", codes.AlreadyExists, http.StatusConflict},
-		{"codes.PermissionDenied", codes.PermissionDenied, http.StatusForbidden},
-		{"codes.Unauthenticated", codes.Unauthenticated, http.StatusUnauthorized},
-		{"codes.ResourceExhausted", codes.ResourceExhausted, http.StatusTooManyRequests},
-		{"codes.FailedPrecondition", codes.FailedPrecondition, http.StatusBadRequest},
-		{"codes.Aborted", codes.Aborted, http.StatusConflict},
-		{"codes.OutOfRange", codes.OutOfRange, http.StatusBadRequest},
-		{"codes.Unimplemented", codes.Unimplemented, http.StatusNotImplemented},
-		{"codes.Internal", codes.Internal, http.StatusInternalServerError},
-		{"codes.Unavailable", codes.Unavailable, http.StatusServiceUnavailable},
-		{"codes.DataLoss", codes.DataLoss, http.StatusInternalServerError},
-		{"else", codes.Code(10000), http.StatusInternalServerError},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := StatusFromGRPCCode(tt.code); got != tt.want {
-				t.Errorf("StatusFromGRPCCode() = %v, want %v", got, tt.want)
-			}
-		})
-	}
-}
-
 func TestContentType(t *testing.T) {
 	tests := []struct {
 		name    string
diff --git a/transport/http/status/status_test.go b/transport/http/status/status_test.go
new file mode 100644
index 00000000..2e2c706b
--- /dev/null
+++ b/transport/http/status/status_test.go
@@ -0,0 +1,71 @@
+package status
+
+import (
+	"net/http"
+	"testing"
+
+	"google.golang.org/grpc/codes"
+)
+
+func TestToGRPCCode(t *testing.T) {
+	tests := []struct {
+		name string
+		code int
+		want codes.Code
+	}{
+		{"http.StatusOK", http.StatusOK, codes.OK},
+		{"http.StatusBadRequest", http.StatusBadRequest, codes.InvalidArgument},
+		{"http.StatusUnauthorized", http.StatusUnauthorized, codes.Unauthenticated},
+		{"http.StatusForbidden", http.StatusForbidden, codes.PermissionDenied},
+		{"http.StatusNotFound", http.StatusNotFound, codes.NotFound},
+		{"http.StatusConflict", http.StatusConflict, codes.Aborted},
+		{"http.StatusTooManyRequests", http.StatusTooManyRequests, codes.ResourceExhausted},
+		{"http.StatusInternalServerError", http.StatusInternalServerError, codes.Internal},
+		{"http.StatusNotImplemented", http.StatusNotImplemented, codes.Unimplemented},
+		{"http.StatusServiceUnavailable", http.StatusServiceUnavailable, codes.Unavailable},
+		{"http.StatusGatewayTimeout", http.StatusGatewayTimeout, codes.DeadlineExceeded},
+		{"StatusClientClosed", ClientClosed, codes.Canceled},
+		{"else", 100000, codes.Unknown},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := ToGRPCCode(tt.code); got != tt.want {
+				t.Errorf("GRPCCodeFromStatus() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestFromGRPCCode(t *testing.T) {
+	tests := []struct {
+		name string
+		code codes.Code
+		want int
+	}{
+		{"codes.OK", codes.OK, http.StatusOK},
+		{"codes.Canceled", codes.Canceled, ClientClosed},
+		{"codes.Unknown", codes.Unknown, http.StatusInternalServerError},
+		{"codes.InvalidArgument", codes.InvalidArgument, http.StatusBadRequest},
+		{"codes.DeadlineExceeded", codes.DeadlineExceeded, http.StatusGatewayTimeout},
+		{"codes.NotFound", codes.NotFound, http.StatusNotFound},
+		{"codes.AlreadyExists", codes.AlreadyExists, http.StatusConflict},
+		{"codes.PermissionDenied", codes.PermissionDenied, http.StatusForbidden},
+		{"codes.Unauthenticated", codes.Unauthenticated, http.StatusUnauthorized},
+		{"codes.ResourceExhausted", codes.ResourceExhausted, http.StatusTooManyRequests},
+		{"codes.FailedPrecondition", codes.FailedPrecondition, http.StatusBadRequest},
+		{"codes.Aborted", codes.Aborted, http.StatusConflict},
+		{"codes.OutOfRange", codes.OutOfRange, http.StatusBadRequest},
+		{"codes.Unimplemented", codes.Unimplemented, http.StatusNotImplemented},
+		{"codes.Internal", codes.Internal, http.StatusInternalServerError},
+		{"codes.Unavailable", codes.Unavailable, http.StatusServiceUnavailable},
+		{"codes.DataLoss", codes.DataLoss, http.StatusInternalServerError},
+		{"else", codes.Code(10000), http.StatusInternalServerError},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := FromGRPCCode(tt.code); got != tt.want {
+				t.Errorf("StatusFromGRPCCode() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}