breaking change: modify API for working with response metadata (#213)
* implement functions to append/get metadata and set/get status code * сhanged behavior to return nil instead of empty metadata for getResponseMetadata() * сhanged work with HTTP headers to use direct array assignment instead of for-range * fix linters * fix meter handler * fix uninitialized response metadata for incoming context * removed a useless test * metrics handler has been fixed to work with compressed data
This commit is contained in:
		
							
								
								
									
										5
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								go.mod
									
									
									
									
									
								
							| @@ -1,8 +1,9 @@ | ||||
| module go.unistack.org/micro-server-http/v4 | ||||
|  | ||||
| go 1.23.0 | ||||
| go 1.24.0 | ||||
|  | ||||
| require ( | ||||
| 	github.com/stretchr/testify v1.10.0 | ||||
| 	go.unistack.org/micro-client-http/v4 v4.1.0 | ||||
| 	go.unistack.org/micro-codec-yaml/v4 v4.1.0 | ||||
| 	go.unistack.org/micro-proto/v4 v4.1.0 | ||||
| @@ -12,10 +13,12 @@ require ( | ||||
|  | ||||
| require ( | ||||
| 	github.com/ash3in/uuidv8 v1.2.0 // indirect | ||||
| 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect | ||||
| 	github.com/google/gnostic v0.7.0 // indirect | ||||
| 	github.com/google/gnostic-models v0.6.9 // indirect | ||||
| 	github.com/google/uuid v1.6.0 // indirect | ||||
| 	github.com/matoous/go-nanoid v1.5.1 // indirect | ||||
| 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect | ||||
| 	github.com/spf13/cast v1.7.1 // indirect | ||||
| 	golang.org/x/sys v0.32.0 // indirect | ||||
| 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect | ||||
|   | ||||
							
								
								
									
										79
									
								
								handler.go
									
									
									
									
									
								
							
							
						
						
									
										79
									
								
								handler.go
									
									
									
									
									
								
							| @@ -103,8 +103,9 @@ func (h *Server) HTTPHandlerFunc(handler interface{}) (http.HandlerFunc, error) | ||||
| 			ct = htype | ||||
| 		} | ||||
|  | ||||
| 		ctx := context.WithValue(r.Context(), rspCodeKey{}, &rspCodeVal{}) | ||||
| 		ctx = context.WithValue(ctx, rspHeaderKey{}, &rspHeaderVal{}) | ||||
| 		ctx := context.WithValue(r.Context(), rspStatusCodeKey{}, &rspStatusCodeVal{}) | ||||
| 		ctx = context.WithValue(ctx, rspMetadataKey{}, &rspMetadataVal{m: metadata.New(0)}) | ||||
|  | ||||
| 		md, ok := metadata.FromIncomingContext(ctx) | ||||
| 		if !ok { | ||||
| 			md = metadata.New(len(r.Header) + 8) | ||||
| @@ -128,6 +129,7 @@ func (h *Server) HTTPHandlerFunc(handler interface{}) (http.HandlerFunc, error) | ||||
| 		} | ||||
|  | ||||
| 		ctx = metadata.NewIncomingContext(ctx, md) | ||||
| 		ctx = metadata.NewOutgoingContext(ctx, metadata.New(0)) | ||||
|  | ||||
| 		path := r.URL.Path | ||||
|  | ||||
| @@ -257,17 +259,6 @@ func (h *Server) HTTPHandlerFunc(handler interface{}) (http.HandlerFunc, error) | ||||
| 				err = rerr.(error) | ||||
| 			} | ||||
|  | ||||
| 			md, ok := metadata.FromOutgoingContext(ctx) | ||||
| 			if !ok { | ||||
| 				md = metadata.New(0) | ||||
| 			} | ||||
| 			if nmd, ok := metadata.FromOutgoingContext(fctx); ok { | ||||
| 				for k, v := range nmd { | ||||
| 					md[k] = append(md[k], v...) | ||||
| 				} | ||||
| 			} | ||||
| 			ctx = metadata.NewOutgoingContext(ctx, md) | ||||
|  | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| @@ -291,19 +282,8 @@ func (h *Server) HTTPHandlerFunc(handler interface{}) (http.HandlerFunc, error) | ||||
| 		appErr := fn(ctx, hr, replyv.Interface()) | ||||
|  | ||||
| 		w.Header().Set(metadata.HeaderContentType, ct) | ||||
| 		if md, ok := metadata.FromOutgoingContext(ctx); ok { | ||||
| 			for k, v := range md { | ||||
| 				for i := range v { | ||||
| 					w.Header().Set(k, v[i]) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		if md := getRspHeader(ctx); md != nil { | ||||
| 			for k, v := range md { | ||||
| 				for _, vv := range v { | ||||
| 					w.Header().Add(k, vv) | ||||
| 				} | ||||
| 			} | ||||
| 		for k, v := range getResponseMetadata(ctx) { | ||||
| 			w.Header()[k] = v | ||||
| 		} | ||||
| 		if nct := w.Header().Get(metadata.HeaderContentType); nct != ct { | ||||
| 			if cf, err = h.newCodec(nct); err != nil { | ||||
| @@ -332,7 +312,7 @@ func (h *Server) HTTPHandlerFunc(handler interface{}) (http.HandlerFunc, error) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if nscode := GetRspCode(ctx); nscode != 0 { | ||||
| 		if nscode := GetResponseStatusCode(ctx); nscode != 0 { | ||||
| 			scode = nscode | ||||
| 		} | ||||
| 		w.WriteHeader(scode) | ||||
| @@ -351,8 +331,8 @@ func (h *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
|  | ||||
| 	ts := time.Now() | ||||
|  | ||||
| 	ctx := context.WithValue(r.Context(), rspCodeKey{}, &rspCodeVal{}) | ||||
| 	ctx = context.WithValue(ctx, rspHeaderKey{}, &rspHeaderVal{}) | ||||
| 	ctx := context.WithValue(r.Context(), rspStatusCodeKey{}, &rspStatusCodeVal{}) | ||||
| 	ctx = context.WithValue(ctx, rspMetadataKey{}, &rspMetadataVal{m: metadata.New(0)}) | ||||
|  | ||||
| 	md, ok := metadata.FromIncomingContext(ctx) | ||||
| 	if !ok { | ||||
| @@ -373,10 +353,11 @@ func (h *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 	md["Proto"] = append(md["Proto"], r.Proto) | ||||
| 	md["Content-Length"] = append(md["Content-Length"], fmt.Sprintf("%d", r.ContentLength)) | ||||
| 	if len(r.TransferEncoding) > 0 { | ||||
| 		md["TransferEncoding"] = append(md["Content-Length"], r.TransferEncoding...) | ||||
| 		md["Transfer-Encoding"] = append(md["Transfer-Encoding"], r.TransferEncoding...) | ||||
| 	} | ||||
| 	md["Host"] = append(md["Host"], r.Host) | ||||
| 	md["RequestURI"] = append(md["RequestURI"], r.RequestURI) | ||||
|  | ||||
| 	ctx = metadata.NewIncomingContext(ctx, md) | ||||
| 	ctx = metadata.NewOutgoingContext(ctx, metadata.New(0)) | ||||
|  | ||||
| @@ -442,7 +423,7 @@ func (h *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 					), | ||||
| 				) | ||||
| 				defer func() { | ||||
| 					n := GetRspCode(ctx) | ||||
| 					n := GetResponseStatusCode(ctx) | ||||
| 					if s, _ := sp.Status(); s != tracer.SpanStatusError && n > 399 { | ||||
| 						sp.SetStatus(tracer.SpanStatusError, http.StatusText(n)) | ||||
| 					} | ||||
| @@ -454,7 +435,7 @@ func (h *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 				h.opts.Meter.Counter(semconv.ServerRequestInflight, "endpoint", endpointName, "server", "http").Inc() | ||||
|  | ||||
| 				defer func() { | ||||
| 					n := GetRspCode(ctx) | ||||
| 					n := GetResponseStatusCode(ctx) | ||||
| 					if n > 399 { | ||||
| 						h.opts.Meter.Counter(semconv.ServerRequestTotal, "endpoint", endpointName, "server", "http", "status", "success", "code", strconv.Itoa(n)).Inc() | ||||
| 					} else { | ||||
| @@ -482,7 +463,7 @@ func (h *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 			) | ||||
|  | ||||
| 			defer func() { | ||||
| 				if n := GetRspCode(ctx); n > 399 { | ||||
| 				if n := GetResponseStatusCode(ctx); n > 399 { | ||||
| 					sp.SetStatus(tracer.SpanStatusError, http.StatusText(n)) | ||||
| 				} else { | ||||
| 					sp.SetStatus(tracer.SpanStatusError, http.StatusText(http.StatusNotFound)) | ||||
| @@ -521,7 +502,7 @@ func (h *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 			h.opts.Meter.Histogram(semconv.ServerRequestDurationSeconds, "endpoint", handler.name, "server", "http").Update(te.Seconds()) | ||||
| 			h.opts.Meter.Counter(semconv.ServerRequestInflight, "endpoint", handler.name, "server", "http").Dec() | ||||
|  | ||||
| 			n := GetRspCode(ctx) | ||||
| 			n := GetResponseStatusCode(ctx) | ||||
| 			if n > 399 { | ||||
| 				h.opts.Meter.Counter(semconv.ServerRequestTotal, "endpoint", handler.name, "server", "http", "status", "failure", "code", strconv.Itoa(n)).Inc() | ||||
| 			} else { | ||||
| @@ -531,7 +512,7 @@ func (h *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 	} | ||||
|  | ||||
| 	defer func() { | ||||
| 		n := GetRspCode(ctx) | ||||
| 		n := GetResponseStatusCode(ctx) | ||||
| 		if n > 399 { | ||||
| 			if s, _ := sp.Status(); s != tracer.SpanStatusError { | ||||
| 				sp.SetStatus(tracer.SpanStatusError, http.StatusText(n)) | ||||
| @@ -625,17 +606,6 @@ func (h *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 			err = rerr.(error) | ||||
| 		} | ||||
|  | ||||
| 		md, ok := metadata.FromOutgoingContext(ctx) | ||||
| 		if !ok { | ||||
| 			md = metadata.New(0) | ||||
| 		} | ||||
| 		if nmd, ok := metadata.FromOutgoingContext(fctx); ok { | ||||
| 			for k, v := range nmd { | ||||
| 				md[k] = append(md[k], v...) | ||||
| 			} | ||||
| 		} | ||||
| 		ctx = metadata.NewOutgoingContext(ctx, md) | ||||
|  | ||||
| 		if err != nil && sp != nil { | ||||
| 			sp.SetStatus(tracer.SpanStatusError, err.Error()) | ||||
| 		} | ||||
| @@ -662,19 +632,8 @@ func (h *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 	appErr := fn(ctx, hr, replyv.Interface()) | ||||
|  | ||||
| 	w.Header().Set(metadata.HeaderContentType, ct) | ||||
| 	if md, ok := metadata.FromOutgoingContext(ctx); ok { | ||||
| 		for k, v := range md { | ||||
| 			for i := range v { | ||||
| 				w.Header().Set(k, v[i]) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	if md := getRspHeader(ctx); md != nil { | ||||
| 		for k, v := range md { | ||||
| 			for _, vv := range v { | ||||
| 				w.Header().Add(k, vv) | ||||
| 			} | ||||
| 		} | ||||
| 	for k, v := range getResponseMetadata(ctx) { | ||||
| 		w.Header()[k] = v | ||||
| 	} | ||||
| 	if nct := w.Header().Get(metadata.HeaderContentType); nct != ct { | ||||
| 		if cf, err = h.newCodec(nct); err != nil { | ||||
| @@ -703,7 +662,7 @@ func (h *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 			handler.sopts.Logger.Error(handler.sopts.Context, "handler error", err) | ||||
| 		} | ||||
| 		scode = http.StatusInternalServerError | ||||
| 	} else if nscode := GetRspCode(ctx); nscode != 0 { | ||||
| 	} else if nscode := GetResponseStatusCode(ctx); nscode != 0 { | ||||
| 		scode = nscode | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import ( | ||||
| 	"sync" | ||||
|  | ||||
| 	codecpb "go.unistack.org/micro-proto/v4/codec" | ||||
| 	httpsrv "go.unistack.org/micro-server-http/v4" | ||||
| 	"go.unistack.org/micro/v4/logger" | ||||
| 	"go.unistack.org/micro/v4/metadata" | ||||
| 	"go.unistack.org/micro/v4/meter" | ||||
| @@ -96,9 +97,9 @@ func (h *Handler) Metrics(ctx context.Context, req *codecpb.Frame, rsp *codecpb. | ||||
|  | ||||
| 	w := io.Writer(buf) | ||||
|  | ||||
| 	if md, ok := metadata.FromOutgoingContext(ctx); gzipAccepted(md) && ok && !h.Options.DisableCompress { | ||||
| 		omd, _ := metadata.FromOutgoingContext(ctx) | ||||
| 		omd.Set(contentEncodingHeader, "gzip") | ||||
| 	if md, ok := metadata.FromIncomingContext(ctx); ok && gzipAccepted(md) && !h.Options.DisableCompress { | ||||
| 		httpsrv.AppendResponseMetadata(ctx, metadata.Pairs(contentEncodingHeader, "gzip")) | ||||
|  | ||||
| 		gz := gzipPool.Get().(*gzip.Writer) | ||||
| 		defer gzipPool.Put(gz) | ||||
|  | ||||
| @@ -106,7 +107,6 @@ func (h *Handler) Metrics(ctx context.Context, req *codecpb.Frame, rsp *codecpb. | ||||
| 		defer gz.Close() | ||||
|  | ||||
| 		w = gz | ||||
| 		gz.Flush() | ||||
| 	} | ||||
|  | ||||
| 	if err := h.Options.Meter.Write(w, h.Options.MeterOptions...); err != nil { | ||||
| @@ -114,6 +114,11 @@ func (h *Handler) Metrics(ctx context.Context, req *codecpb.Frame, rsp *codecpb. | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	// gz.Flush() must be called after writing metrics to ensure buffered data is written to the underlying writer. | ||||
| 	if gz, ok := w.(*gzip.Writer); ok { | ||||
| 		gz.Flush() | ||||
| 	} | ||||
|  | ||||
| 	rsp.Data = buf.Bytes() | ||||
|  | ||||
| 	return nil | ||||
|   | ||||
| @@ -1,11 +0,0 @@ | ||||
| package http | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestHandler(t *testing.T) { | ||||
| 	ctx := context.WithValue(context.TODO(), rspCodeKey{}, &rspCodeVal{}) | ||||
| 	SetRspCode(ctx, 404) | ||||
| } | ||||
							
								
								
									
										47
									
								
								metadata.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								metadata.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| package http | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"go.unistack.org/micro/v4/metadata" | ||||
| ) | ||||
|  | ||||
| type ( | ||||
| 	rspMetadataKey struct{} | ||||
| 	rspMetadataVal struct { | ||||
| 		m metadata.Metadata | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| // AppendResponseMetadata adds metadata entries to metadata.Metadata stored in the context. | ||||
| // It expects the context to contain a *rspMetadataVal value under the rspMetadataKey{} key. | ||||
| // If the value is missing or invalid, the function does nothing. | ||||
| // | ||||
| // Note: this function is not thread-safe. Synchronization is required if used from multiple goroutines. | ||||
| func AppendResponseMetadata(ctx context.Context, md metadata.Metadata) { | ||||
| 	if md == nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	val, ok := ctx.Value(rspMetadataKey{}).(*rspMetadataVal) | ||||
| 	if !ok || val == nil || val.m == nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	for key, values := range md { | ||||
| 		val.m.Append(key, values...) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // getResponseMetadata retrieves the metadata.Metadata stored in the context. | ||||
| // | ||||
| // Note: this function is not thread-safe. Synchronization is required if used from multiple goroutines. | ||||
| // If you plan to modify the returned metadata, make a full copy to avoid affecting shared state. | ||||
| func getResponseMetadata(ctx context.Context) metadata.Metadata { | ||||
| 	val, ok := ctx.Value(rspMetadataKey{}).(*rspMetadataVal) | ||||
| 	if !ok || val == nil || val.m == nil { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	return val.m | ||||
| } | ||||
							
								
								
									
										136
									
								
								metadata_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								metadata_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,136 @@ | ||||
| package http | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/require" | ||||
| 	"go.unistack.org/micro/v4/metadata" | ||||
| ) | ||||
|  | ||||
| func TestAppendResponseMetadata(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name     string | ||||
| 		ctx      context.Context | ||||
| 		md       metadata.Metadata | ||||
| 		expected context.Context | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:     "nil metadata", | ||||
| 			ctx:      context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{m: metadata.Metadata{}}), | ||||
| 			md:       nil, | ||||
| 			expected: context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{m: metadata.Metadata{}}), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "empty metadata", | ||||
| 			ctx:      context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{m: metadata.Metadata{}}), | ||||
| 			md:       metadata.Metadata{}, | ||||
| 			expected: context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{m: metadata.Metadata{}}), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "context without response metadata key", | ||||
| 			ctx:      context.Background(), | ||||
| 			md:       metadata.Pairs("key1", "val1"), | ||||
| 			expected: context.Background(), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "context with nil response metadata value", | ||||
| 			ctx:      context.WithValue(context.Background(), rspMetadataKey{}, nil), | ||||
| 			md:       metadata.Pairs("key1", "val1"), | ||||
| 			expected: context.WithValue(context.Background(), rspMetadataKey{}, nil), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "context with incorrect type in response metadata value", | ||||
| 			ctx:      context.WithValue(context.Background(), rspMetadataKey{}, struct{}{}), | ||||
| 			md:       metadata.Pairs("key1", "val1"), | ||||
| 			expected: context.WithValue(context.Background(), rspMetadataKey{}, struct{}{}), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "context with response metadata value, but nil metadata", | ||||
| 			ctx:      context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{m: nil}), | ||||
| 			md:       metadata.Pairs("key1", "val1"), | ||||
| 			expected: context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{m: nil}), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "basic metadata append", | ||||
| 			ctx:  context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{m: metadata.Metadata{}}), | ||||
| 			md:   metadata.Pairs("key1", "val1"), | ||||
| 			expected: context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{ | ||||
| 				m: metadata.Metadata{ | ||||
| 					"key1": []string{"val1"}, | ||||
| 				}, | ||||
| 			}), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "multiple values for same key", | ||||
| 			ctx:  context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{m: metadata.Metadata{}}), | ||||
| 			md:   metadata.Pairs("key1", "val1", "key1", "val2"), | ||||
| 			expected: context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{ | ||||
| 				m: metadata.Metadata{ | ||||
| 					"key1": []string{"val1", "val2"}, | ||||
| 				}, | ||||
| 			}), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "multiple values for different keys", | ||||
| 			ctx:  context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{m: metadata.Metadata{}}), | ||||
| 			md:   metadata.Pairs("key1", "val1", "key1", "val2", "key2", "val3", "key2", "val4", "key3", "val5"), | ||||
| 			expected: context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{ | ||||
| 				m: metadata.Metadata{ | ||||
| 					"key1": []string{"val1", "val2"}, | ||||
| 					"key2": []string{"val3", "val4"}, | ||||
| 					"key3": []string{"val5"}, | ||||
| 				}, | ||||
| 			}), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			AppendResponseMetadata(tt.ctx, tt.md) | ||||
| 			require.Equal(t, tt.expected, tt.ctx) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetResponseMetadata(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name     string | ||||
| 		ctx      context.Context | ||||
| 		expected metadata.Metadata | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:     "context without response metadata key", | ||||
| 			ctx:      context.Background(), | ||||
| 			expected: nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "context with nil response metadata value", | ||||
| 			ctx:      context.WithValue(context.Background(), rspMetadataKey{}, nil), | ||||
| 			expected: nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "context with incorrect type in response metadata value", | ||||
| 			ctx:      context.WithValue(context.Background(), rspMetadataKey{}, &struct{}{}), | ||||
| 			expected: nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "context with response metadata value, but nil metadata", | ||||
| 			ctx:      context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{m: nil}), | ||||
| 			expected: nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "valid metadata", | ||||
| 			ctx: context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{ | ||||
| 				m: metadata.Pairs("key1", "value1"), | ||||
| 			}), | ||||
| 			expected: metadata.Metadata{"key1": {"value1"}}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			require.Equal(t, tt.expected, getResponseMetadata(tt.ctx)) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										45
									
								
								options.go
									
									
									
									
									
								
							
							
						
						
									
										45
									
								
								options.go
									
									
									
									
									
								
							| @@ -31,51 +31,6 @@ func (err *Error) Error() string { | ||||
| 	return fmt.Sprintf("%v", err.err) | ||||
| } | ||||
|  | ||||
| type ( | ||||
| 	rspCodeKey struct{} | ||||
| 	rspCodeVal struct { | ||||
| 		code int | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| type ( | ||||
| 	rspHeaderKey struct{} | ||||
| 	rspHeaderVal struct { | ||||
| 		h http.Header | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| // SetRspHeader add response headers | ||||
| func SetRspHeader(ctx context.Context, h http.Header) { | ||||
| 	if rsp, ok := ctx.Value(rspHeaderKey{}).(*rspHeaderVal); ok { | ||||
| 		rsp.h = h | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // SetRspCode saves response code in context, must be used by handler to specify http code | ||||
| func SetRspCode(ctx context.Context, code int) { | ||||
| 	if rsp, ok := ctx.Value(rspCodeKey{}).(*rspCodeVal); ok { | ||||
| 		rsp.code = code | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // getRspHeader get http.Header from context | ||||
| func getRspHeader(ctx context.Context) http.Header { | ||||
| 	if rsp, ok := ctx.Value(rspHeaderKey{}).(*rspHeaderVal); ok { | ||||
| 		return rsp.h | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // GetRspCode used internally by generated http server handler | ||||
| func GetRspCode(ctx context.Context) int { | ||||
| 	code := int(200) | ||||
| 	if rsp, ok := ctx.Value(rspCodeKey{}).(*rspCodeVal); ok { | ||||
| 		code = rsp.code | ||||
| 	} | ||||
| 	return code | ||||
| } | ||||
|  | ||||
| type middlewareKey struct{} | ||||
|  | ||||
| // Middleware passes http middlewares | ||||
|   | ||||
							
								
								
									
										29
									
								
								response.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								response.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| package http | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"net/http" | ||||
| ) | ||||
|  | ||||
| type ( | ||||
| 	rspStatusCodeKey struct{} | ||||
| 	rspStatusCodeVal struct { | ||||
| 		code int | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| // SetResponseStatusCode sets the status code in the context. | ||||
| func SetResponseStatusCode(ctx context.Context, code int) { | ||||
| 	if rsp, ok := ctx.Value(rspStatusCodeKey{}).(*rspStatusCodeVal); ok { | ||||
| 		rsp.code = code | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // GetResponseStatusCode retrieves the response status code from the context. | ||||
| func GetResponseStatusCode(ctx context.Context) int { | ||||
| 	code := http.StatusOK | ||||
| 	if rsp, ok := ctx.Value(rspStatusCodeKey{}).(*rspStatusCodeVal); ok { | ||||
| 		code = rsp.code | ||||
| 	} | ||||
| 	return code | ||||
| } | ||||
							
								
								
									
										79
									
								
								response_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								response_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| package http | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"net/http" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| func TestSetResponseStatusCode(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name     string | ||||
| 		ctx      context.Context | ||||
| 		code     int | ||||
| 		expected context.Context | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:     "context without response status code key", | ||||
| 			ctx:      context.Background(), | ||||
| 			code:     http.StatusOK, | ||||
| 			expected: context.Background(), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "context with incorrect type in response status code value", | ||||
| 			ctx:      context.WithValue(context.Background(), rspStatusCodeKey{}, struct{}{}), | ||||
| 			code:     http.StatusOK, | ||||
| 			expected: context.WithValue(context.Background(), rspStatusCodeKey{}, struct{}{}), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "successfully set response status code", | ||||
| 			ctx:      context.WithValue(context.Background(), rspStatusCodeKey{}, &rspStatusCodeVal{}), | ||||
| 			code:     http.StatusOK, | ||||
| 			expected: context.WithValue(context.Background(), rspStatusCodeKey{}, &rspStatusCodeVal{code: http.StatusOK}), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			SetResponseStatusCode(tt.ctx, tt.code) | ||||
| 			require.Equal(t, tt.expected, tt.ctx) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetResponseStatusCode(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name     string | ||||
| 		ctx      context.Context | ||||
| 		expected int | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:     "no value in context, should return 200", | ||||
| 			ctx:      context.Background(), | ||||
| 			expected: http.StatusOK, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "context with nil value", | ||||
| 			ctx:      context.WithValue(context.Background(), rspStatusCodeKey{}, nil), | ||||
| 			expected: http.StatusOK, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "context with wrong type", | ||||
| 			ctx:      context.WithValue(context.Background(), rspStatusCodeKey{}, struct{}{}), | ||||
| 			expected: http.StatusOK, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "context with valid status code", | ||||
| 			ctx:      context.WithValue(context.Background(), rspStatusCodeKey{}, &rspStatusCodeVal{code: http.StatusNotFound}), | ||||
| 			expected: http.StatusNotFound, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			require.Equal(t, tt.expected, GetResponseStatusCode(tt.ctx)) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user