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 | module go.unistack.org/micro-server-http/v4 | ||||||
|  |  | ||||||
| go 1.23.0 | go 1.24.0 | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
|  | 	github.com/stretchr/testify v1.10.0 | ||||||
| 	go.unistack.org/micro-client-http/v4 v4.1.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-codec-yaml/v4 v4.1.0 | ||||||
| 	go.unistack.org/micro-proto/v4 v4.1.0 | 	go.unistack.org/micro-proto/v4 v4.1.0 | ||||||
| @@ -12,10 +13,12 @@ require ( | |||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/ash3in/uuidv8 v1.2.0 // indirect | 	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 v0.7.0 // indirect | ||||||
| 	github.com/google/gnostic-models v0.6.9 // indirect | 	github.com/google/gnostic-models v0.6.9 // indirect | ||||||
| 	github.com/google/uuid v1.6.0 // indirect | 	github.com/google/uuid v1.6.0 // indirect | ||||||
| 	github.com/matoous/go-nanoid v1.5.1 // 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 | 	github.com/spf13/cast v1.7.1 // indirect | ||||||
| 	golang.org/x/sys v0.32.0 // indirect | 	golang.org/x/sys v0.32.0 // indirect | ||||||
| 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // 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 | 			ct = htype | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		ctx := context.WithValue(r.Context(), rspCodeKey{}, &rspCodeVal{}) | 		ctx := context.WithValue(r.Context(), rspStatusCodeKey{}, &rspStatusCodeVal{}) | ||||||
| 		ctx = context.WithValue(ctx, rspHeaderKey{}, &rspHeaderVal{}) | 		ctx = context.WithValue(ctx, rspMetadataKey{}, &rspMetadataVal{m: metadata.New(0)}) | ||||||
|  |  | ||||||
| 		md, ok := metadata.FromIncomingContext(ctx) | 		md, ok := metadata.FromIncomingContext(ctx) | ||||||
| 		if !ok { | 		if !ok { | ||||||
| 			md = metadata.New(len(r.Header) + 8) | 			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.NewIncomingContext(ctx, md) | ||||||
|  | 		ctx = metadata.NewOutgoingContext(ctx, metadata.New(0)) | ||||||
|  |  | ||||||
| 		path := r.URL.Path | 		path := r.URL.Path | ||||||
|  |  | ||||||
| @@ -257,17 +259,6 @@ func (h *Server) HTTPHandlerFunc(handler interface{}) (http.HandlerFunc, error) | |||||||
| 				err = rerr.(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 | 			return err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -291,19 +282,8 @@ func (h *Server) HTTPHandlerFunc(handler interface{}) (http.HandlerFunc, error) | |||||||
| 		appErr := fn(ctx, hr, replyv.Interface()) | 		appErr := fn(ctx, hr, replyv.Interface()) | ||||||
|  |  | ||||||
| 		w.Header().Set(metadata.HeaderContentType, ct) | 		w.Header().Set(metadata.HeaderContentType, ct) | ||||||
| 		if md, ok := metadata.FromOutgoingContext(ctx); ok { | 		for k, v := range getResponseMetadata(ctx) { | ||||||
| 			for k, v := range md { | 			w.Header()[k] = v | ||||||
| 				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) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
| 		if nct := w.Header().Get(metadata.HeaderContentType); nct != ct { | 		if nct := w.Header().Get(metadata.HeaderContentType); nct != ct { | ||||||
| 			if cf, err = h.newCodec(nct); err != nil { | 			if cf, err = h.newCodec(nct); err != nil { | ||||||
| @@ -332,7 +312,7 @@ func (h *Server) HTTPHandlerFunc(handler interface{}) (http.HandlerFunc, error) | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if nscode := GetRspCode(ctx); nscode != 0 { | 		if nscode := GetResponseStatusCode(ctx); nscode != 0 { | ||||||
| 			scode = nscode | 			scode = nscode | ||||||
| 		} | 		} | ||||||
| 		w.WriteHeader(scode) | 		w.WriteHeader(scode) | ||||||
| @@ -351,8 +331,8 @@ func (h *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||||||
|  |  | ||||||
| 	ts := time.Now() | 	ts := time.Now() | ||||||
|  |  | ||||||
| 	ctx := context.WithValue(r.Context(), rspCodeKey{}, &rspCodeVal{}) | 	ctx := context.WithValue(r.Context(), rspStatusCodeKey{}, &rspStatusCodeVal{}) | ||||||
| 	ctx = context.WithValue(ctx, rspHeaderKey{}, &rspHeaderVal{}) | 	ctx = context.WithValue(ctx, rspMetadataKey{}, &rspMetadataVal{m: metadata.New(0)}) | ||||||
|  |  | ||||||
| 	md, ok := metadata.FromIncomingContext(ctx) | 	md, ok := metadata.FromIncomingContext(ctx) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| @@ -373,10 +353,11 @@ func (h *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||||||
| 	md["Proto"] = append(md["Proto"], r.Proto) | 	md["Proto"] = append(md["Proto"], r.Proto) | ||||||
| 	md["Content-Length"] = append(md["Content-Length"], fmt.Sprintf("%d", r.ContentLength)) | 	md["Content-Length"] = append(md["Content-Length"], fmt.Sprintf("%d", r.ContentLength)) | ||||||
| 	if len(r.TransferEncoding) > 0 { | 	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["Host"] = append(md["Host"], r.Host) | ||||||
| 	md["RequestURI"] = append(md["RequestURI"], r.RequestURI) | 	md["RequestURI"] = append(md["RequestURI"], r.RequestURI) | ||||||
|  |  | ||||||
| 	ctx = metadata.NewIncomingContext(ctx, md) | 	ctx = metadata.NewIncomingContext(ctx, md) | ||||||
| 	ctx = metadata.NewOutgoingContext(ctx, metadata.New(0)) | 	ctx = metadata.NewOutgoingContext(ctx, metadata.New(0)) | ||||||
|  |  | ||||||
| @@ -442,7 +423,7 @@ func (h *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||||||
| 					), | 					), | ||||||
| 				) | 				) | ||||||
| 				defer func() { | 				defer func() { | ||||||
| 					n := GetRspCode(ctx) | 					n := GetResponseStatusCode(ctx) | ||||||
| 					if s, _ := sp.Status(); s != tracer.SpanStatusError && n > 399 { | 					if s, _ := sp.Status(); s != tracer.SpanStatusError && n > 399 { | ||||||
| 						sp.SetStatus(tracer.SpanStatusError, http.StatusText(n)) | 						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() | 				h.opts.Meter.Counter(semconv.ServerRequestInflight, "endpoint", endpointName, "server", "http").Inc() | ||||||
|  |  | ||||||
| 				defer func() { | 				defer func() { | ||||||
| 					n := GetRspCode(ctx) | 					n := GetResponseStatusCode(ctx) | ||||||
| 					if n > 399 { | 					if n > 399 { | ||||||
| 						h.opts.Meter.Counter(semconv.ServerRequestTotal, "endpoint", endpointName, "server", "http", "status", "success", "code", strconv.Itoa(n)).Inc() | 						h.opts.Meter.Counter(semconv.ServerRequestTotal, "endpoint", endpointName, "server", "http", "status", "success", "code", strconv.Itoa(n)).Inc() | ||||||
| 					} else { | 					} else { | ||||||
| @@ -482,7 +463,7 @@ func (h *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||||||
| 			) | 			) | ||||||
|  |  | ||||||
| 			defer func() { | 			defer func() { | ||||||
| 				if n := GetRspCode(ctx); n > 399 { | 				if n := GetResponseStatusCode(ctx); n > 399 { | ||||||
| 					sp.SetStatus(tracer.SpanStatusError, http.StatusText(n)) | 					sp.SetStatus(tracer.SpanStatusError, http.StatusText(n)) | ||||||
| 				} else { | 				} else { | ||||||
| 					sp.SetStatus(tracer.SpanStatusError, http.StatusText(http.StatusNotFound)) | 					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.Histogram(semconv.ServerRequestDurationSeconds, "endpoint", handler.name, "server", "http").Update(te.Seconds()) | ||||||
| 			h.opts.Meter.Counter(semconv.ServerRequestInflight, "endpoint", handler.name, "server", "http").Dec() | 			h.opts.Meter.Counter(semconv.ServerRequestInflight, "endpoint", handler.name, "server", "http").Dec() | ||||||
|  |  | ||||||
| 			n := GetRspCode(ctx) | 			n := GetResponseStatusCode(ctx) | ||||||
| 			if n > 399 { | 			if n > 399 { | ||||||
| 				h.opts.Meter.Counter(semconv.ServerRequestTotal, "endpoint", handler.name, "server", "http", "status", "failure", "code", strconv.Itoa(n)).Inc() | 				h.opts.Meter.Counter(semconv.ServerRequestTotal, "endpoint", handler.name, "server", "http", "status", "failure", "code", strconv.Itoa(n)).Inc() | ||||||
| 			} else { | 			} else { | ||||||
| @@ -531,7 +512,7 @@ func (h *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	defer func() { | 	defer func() { | ||||||
| 		n := GetRspCode(ctx) | 		n := GetResponseStatusCode(ctx) | ||||||
| 		if n > 399 { | 		if n > 399 { | ||||||
| 			if s, _ := sp.Status(); s != tracer.SpanStatusError { | 			if s, _ := sp.Status(); s != tracer.SpanStatusError { | ||||||
| 				sp.SetStatus(tracer.SpanStatusError, http.StatusText(n)) | 				sp.SetStatus(tracer.SpanStatusError, http.StatusText(n)) | ||||||
| @@ -625,17 +606,6 @@ func (h *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||||||
| 			err = rerr.(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) |  | ||||||
|  |  | ||||||
| 		if err != nil && sp != nil { | 		if err != nil && sp != nil { | ||||||
| 			sp.SetStatus(tracer.SpanStatusError, err.Error()) | 			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()) | 	appErr := fn(ctx, hr, replyv.Interface()) | ||||||
|  |  | ||||||
| 	w.Header().Set(metadata.HeaderContentType, ct) | 	w.Header().Set(metadata.HeaderContentType, ct) | ||||||
| 	if md, ok := metadata.FromOutgoingContext(ctx); ok { | 	for k, v := range getResponseMetadata(ctx) { | ||||||
| 		for k, v := range md { | 		w.Header()[k] = v | ||||||
| 			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) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| 	if nct := w.Header().Get(metadata.HeaderContentType); nct != ct { | 	if nct := w.Header().Get(metadata.HeaderContentType); nct != ct { | ||||||
| 		if cf, err = h.newCodec(nct); err != nil { | 		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) | 			handler.sopts.Logger.Error(handler.sopts.Context, "handler error", err) | ||||||
| 		} | 		} | ||||||
| 		scode = http.StatusInternalServerError | 		scode = http.StatusInternalServerError | ||||||
| 	} else if nscode := GetRspCode(ctx); nscode != 0 { | 	} else if nscode := GetResponseStatusCode(ctx); nscode != 0 { | ||||||
| 		scode = nscode | 		scode = nscode | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ import ( | |||||||
| 	"sync" | 	"sync" | ||||||
|  |  | ||||||
| 	codecpb "go.unistack.org/micro-proto/v4/codec" | 	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/logger" | ||||||
| 	"go.unistack.org/micro/v4/metadata" | 	"go.unistack.org/micro/v4/metadata" | ||||||
| 	"go.unistack.org/micro/v4/meter" | 	"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) | 	w := io.Writer(buf) | ||||||
|  |  | ||||||
| 	if md, ok := metadata.FromOutgoingContext(ctx); gzipAccepted(md) && ok && !h.Options.DisableCompress { | 	if md, ok := metadata.FromIncomingContext(ctx); ok && gzipAccepted(md) && !h.Options.DisableCompress { | ||||||
| 		omd, _ := metadata.FromOutgoingContext(ctx) | 		httpsrv.AppendResponseMetadata(ctx, metadata.Pairs(contentEncodingHeader, "gzip")) | ||||||
| 		omd.Set(contentEncodingHeader, "gzip") |  | ||||||
| 		gz := gzipPool.Get().(*gzip.Writer) | 		gz := gzipPool.Get().(*gzip.Writer) | ||||||
| 		defer gzipPool.Put(gz) | 		defer gzipPool.Put(gz) | ||||||
|  |  | ||||||
| @@ -106,7 +107,6 @@ func (h *Handler) Metrics(ctx context.Context, req *codecpb.Frame, rsp *codecpb. | |||||||
| 		defer gz.Close() | 		defer gz.Close() | ||||||
|  |  | ||||||
| 		w = gz | 		w = gz | ||||||
| 		gz.Flush() |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := h.Options.Meter.Write(w, h.Options.MeterOptions...); err != nil { | 	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 | 		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() | 	rsp.Data = buf.Bytes() | ||||||
|  |  | ||||||
| 	return nil | 	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) | 	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{} | type middlewareKey struct{} | ||||||
|  |  | ||||||
| // Middleware passes http middlewares | // 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