-
Notifications
You must be signed in to change notification settings - Fork 4
feat: improve HTTP span tracing with route patterns, status codes, and semconv v1.40.0 #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6755b18
0b64e49
91ad35b
f705ea1
03b739d
96f996b
567810b
ab78d2d
c810545
d0cc95c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ import ( | |
| "net" | ||
| "net/http" | ||
| "net/http/pprof" | ||
| "strconv" | ||
| "strings" | ||
| "sync" | ||
| "time" | ||
|
|
@@ -24,8 +25,10 @@ import ( | |
| "github.com/prometheus/client_golang/prometheus/promhttp" | ||
| "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" | ||
| "go.opentelemetry.io/otel" | ||
| "go.opentelemetry.io/otel/attribute" | ||
| "go.opentelemetry.io/otel/codes" | ||
| "go.opentelemetry.io/otel/propagation" | ||
| semconv "go.opentelemetry.io/otel/semconv/v1.12.0" | ||
| semconv "go.opentelemetry.io/otel/semconv/v1.40.0" | ||
| oteltrace "go.opentelemetry.io/otel/trace" | ||
| "google.golang.org/grpc" | ||
| "google.golang.org/grpc/credentials" | ||
|
|
@@ -169,37 +172,113 @@ func (c *cb) processConfig() { | |
| } | ||
| } | ||
|
|
||
| // statusRecorder wraps http.ResponseWriter to capture the final HTTP status code. | ||
| // It records the first status >= 200, plus 101 Switching Protocols (which is | ||
| // terminal). Other 1xx statuses are informational and skipped. | ||
| // Unwrap() is provided for http.ResponseController (Go 1.20+) to access optional | ||
| // interfaces (http.Flusher, http.Hijacker, etc.) from the underlying writer. | ||
| type statusRecorder struct { | ||
| http.ResponseWriter | ||
| status int | ||
| wroteHeader bool | ||
| } | ||
|
|
||
| func (sr *statusRecorder) WriteHeader(code int) { | ||
| if !sr.wroteHeader && (code >= 200 || code == http.StatusSwitchingProtocols) { | ||
| sr.status = code | ||
| sr.wroteHeader = true | ||
| } | ||
| sr.ResponseWriter.WriteHeader(code) | ||
|
ankurs marked this conversation as resolved.
|
||
| } | ||
|
ankurs marked this conversation as resolved.
ankurs marked this conversation as resolved.
|
||
|
|
||
| func (sr *statusRecorder) Write(b []byte) (int, error) { | ||
| if !sr.wroteHeader { | ||
| sr.status = http.StatusOK | ||
| sr.wroteHeader = true | ||
| } | ||
| return sr.ResponseWriter.Write(b) | ||
| } | ||
|
|
||
| // Unwrap returns the underlying ResponseWriter so that http.ResponseController | ||
| // and middleware can access optional interfaces (http.Flusher, http.Hijacker, etc.). | ||
| func (sr *statusRecorder) Unwrap() http.ResponseWriter { | ||
| return sr.ResponseWriter | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| // endSpan records the HTTP status code on the span, marks it as error for 5xx, and ends it. | ||
| func endSpan(span oteltrace.Span, rec *statusRecorder) { | ||
| span.SetAttributes(semconv.HTTPResponseStatusCode(rec.status)) | ||
| if rec.status >= 500 { | ||
| span.SetStatus(codes.Error, http.StatusText(rec.status)) | ||
| } | ||
| span.End() | ||
| } | ||
|
|
||
| // httpSpanAttributes returns the OTEL attributes for an incoming HTTP request, | ||
| // omitting empty-valued attributes (e.g. scheme behind a reverse proxy). | ||
| func httpSpanAttributes(r *http.Request) []attribute.KeyValue { | ||
| host, port, err := net.SplitHostPort(r.Host) | ||
| if err != nil { | ||
| host = r.Host | ||
| } | ||
| host = strings.TrimPrefix(strings.TrimSuffix(host, "]"), "[") | ||
| attrs := []attribute.KeyValue{ | ||
| semconv.HTTPRequestMethodKey.String(r.Method), | ||
| semconv.URLPath(r.URL.Path), | ||
| semconv.ServerAddress(host), | ||
| } | ||
|
ankurs marked this conversation as resolved.
|
||
| if port != "" { | ||
| if p, err := strconv.Atoi(port); err == nil { | ||
| attrs = append(attrs, semconv.ServerPort(p)) | ||
| } | ||
| } | ||
|
ankurs marked this conversation as resolved.
|
||
| if r.URL.RawQuery != "" { | ||
| attrs = append(attrs, semconv.URLQuery(r.URL.RawQuery)) | ||
|
ankurs marked this conversation as resolved.
|
||
| } | ||
| if r.URL.Scheme != "" { | ||
| attrs = append(attrs, semconv.URLScheme(r.URL.Scheme)) | ||
| } | ||
|
Comment on lines
+219
to
+239
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's locate and examine the core.go file
find . -name "core.go" -type f | head -20Repository: go-coldbrew/core Length of output: 69 🏁 Script executed: # Look for the httpSpanAttributes function to confirm the code matches the review
rg -n "func httpSpanAttributes" --type go -A 25Repository: go-coldbrew/core Length of output: 1131 🏁 Script executed: # Check if there's any proxy header handling (X-Forwarded-Proto, etc.) in the codebase
rg -i "forwarded-proto|x-forwarded|proto" --type goRepository: go-coldbrew/core Length of output: 4179 🌐 Web query:
💡 Result: In Go's net/http server, for inbound requests, http.Request.URL.Scheme is always an empty string (""). The server does not set it. The URL field in http.Request is populated by parsing the request-target from the HTTP Request-Line (stored in RequestURI) using url.ParseRequestURI (for HTTP/1.x via readRequest in request.go). Per RFC 7230 Section 5.3, request-targets in HTTP/1.x are typically relative URIs (absolute-path [ "?" query ]) without scheme or authority for non-authority forms (like origin-form used in direct requests). Thus, when parsed, the Scheme field remains empty, while Path and RawQuery are populated. Official documentation confirms: "For server requests, the URL is parsed from the URI supplied on the Request-Line as stored in RequestURI. For most requests, fields other than Path and RawQuery will be empty." (pkg.go.dev/net/http#Request) The scheme (http or https) is determined by the connection: check if r.TLS != nil ("https") or nil ("http"). The server sets r.TLS for TLS-enabled connections before invoking the handler. Behind proxies/load balancers, use headers like X-Forwarded-Proto (via Server.ProxyHeaders if configured). To construct full URL: scheme + "://" + r.Host + r.URL.RequestURI Citations:
Derive On inbound server requests, if r.URL.RawQuery != "" {
attrs = append(attrs, semconv.URLQuery(r.URL.RawQuery))
}
- if r.URL.Scheme != "" {
- attrs = append(attrs, semconv.URLScheme(r.URL.Scheme))
- }
+ switch {
+ case r.URL.Scheme != "":
+ attrs = append(attrs, semconv.URLScheme(r.URL.Scheme))
+ case r.TLS != nil:
+ attrs = append(attrs, semconv.URLScheme("https"))
+ default:
+ attrs = append(attrs, semconv.URLScheme("http"))
+ }🤖 Prompt for AI Agents |
||
| return attrs | ||
| } | ||
|
|
||
| // tracingWrapper is a middleware that creates a new OTEL span for each incoming HTTP request. | ||
| // It extracts any propagated trace context from the request headers and, for non-filtered | ||
| // methods, starts a server span that is attached to the request context. | ||
| func tracingWrapper(h http.Handler) http.Handler { | ||
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| prop := otel.GetTextMapPropagator() | ||
| ctx := prop.Extract(r.Context(), propagation.HeaderCarrier(r.Header)) | ||
| ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header)) | ||
|
|
||
| if interceptors.FilterMethodsFunc(ctx, r.URL.Path) { | ||
| var serverSpan oteltrace.Span | ||
| ctx, serverSpan = otel.Tracer("coldbrew-http").Start(ctx, "ServeHTTP", | ||
| ctx, serverSpan = otel.Tracer("coldbrew-http").Start(ctx, r.Method, | ||
| oteltrace.WithSpanKind(oteltrace.SpanKindServer), | ||
| oteltrace.WithAttributes( | ||
| semconv.HTTPMethodKey.String(r.Method), | ||
| semconv.HTTPTargetKey.String(r.URL.RequestURI()), | ||
| ), | ||
| oteltrace.WithAttributes(httpSpanAttributes(r)...), | ||
| ) | ||
|
ankurs marked this conversation as resolved.
|
||
| r = r.WithContext(ctx) | ||
| defer serverSpan.End() | ||
| } else { | ||
| r = r.WithContext(ctx) | ||
| rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK} | ||
| w = rec | ||
| defer endSpan(serverSpan, rec) | ||
| } | ||
|
|
||
| _, han := interceptors.NRHttpTracer("", h.ServeHTTP) | ||
| // add this info to log | ||
| ctx = r.Context() | ||
| ctx = options.AddToOptions(ctx, "", "") | ||
| ctx = loggers.AddToLogContext(ctx, "httpPath", r.URL.Path) | ||
| r = r.WithContext(ctx) | ||
| han(w, r) | ||
| han(w, r.WithContext(ctx)) | ||
|
Comment on lines
+248
to
+264
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: git ls-files | grep -E "\.go$" | head -20Repository: go-coldbrew/core Length of output: 173 🏁 Script executed: # Find the core.go file and examine the statusRecorder and endSpan implementations
fd -e go -name "core.go"Repository: go-coldbrew/core Length of output: 290 🏁 Script executed: # Get the statusRecorder and endSpan definitions
rg "type statusRecorder" -A 20Repository: go-coldbrew/core Length of output: 702 🏁 Script executed: # Find the endSpan function
rg "func endSpan" -A 15Repository: go-coldbrew/core Length of output: 772 🏁 Script executed: # Look at the full context around lines 248-264
sed -n '240,270p' core.goRepository: go-coldbrew/core Length of output: 1302 🏁 Script executed: # Check if there are any tests that verify panic handling
rg "panic" core_test.go -B 3 -A 10Repository: go-coldbrew/core Length of output: 42 🏁 Script executed: # Look for any existing panic recovery in the codebase
rg "recover\(\)" -B 3 -A 3Repository: go-coldbrew/core Length of output: 832 🌐 Web query:
💡 Result: No, the standard Go net/http server does not write a 500 status code when recovering from a panic in a handler. Instead, it recovers from the panic (except for http.ErrAbortHandler), logs the panic details and stack trace to the server error log with a message like "http: panic serving [addr]: [error]\n[stack]", and then aborts the response by closing the network connection (for HTTP/1) or sending an HTTP/2 RST_STREAM. This typically results in the client receiving no response body and either a connection close (which may appear as an empty reply or connection reset, depending on the client) or an empty/incomplete response without a 500 status. This behavior is intentional to isolate the failed request and prevent it from crashing the server process, as documented in the net/http package and visible in the server.go source code around the defer recover in (*conn).serve. Custom middleware is commonly used to instead return a proper 500 Internal Server Error response, log the error, and continue. Citations:
🏁 Script executed: rg "NRHttpTracer" -B 2 -A 10Repository: go-coldbrew/core Length of output: 678 🏁 Script executed: # Also search for its definition or implementation
rg "func NRHttpTracer" -A 20Repository: go-coldbrew/core Length of output: 42 Unhandled panics will be recorded as successful (200 status) in traces. If the wrapped handler panics before writing a response, the defer executes during panic unwinding and records the span with the initial status (200). Since Suggested fix if interceptors.FilterMethodsFunc(ctx, r.URL.Path) {
var serverSpan oteltrace.Span
ctx, serverSpan = otel.Tracer("coldbrew-http").Start(ctx, r.Method,
oteltrace.WithSpanKind(oteltrace.SpanKindServer),
oteltrace.WithAttributes(httpSpanAttributes(r)...),
)
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
w = rec
- defer endSpan(serverSpan, rec)
+ defer func() {
+ if p := recover(); p != nil {
+ if !rec.wroteHeader {
+ rec.status = http.StatusInternalServerError
+ rec.wroteHeader = true
+ }
+ endSpan(serverSpan, rec)
+ panic(p)
+ }
+ endSpan(serverSpan, rec)
+ }()
}🤖 Prompt for AI Agents |
||
| }) | ||
| } | ||
|
|
||
| // spanRouteMiddleware is a grpc-gateway middleware that updates the OTEL span | ||
| // name and http.route attribute with the matched route pattern after routing. | ||
| func spanRouteMiddleware(next runtime.HandlerFunc) runtime.HandlerFunc { | ||
| return func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) { | ||
| if pattern, ok := runtime.HTTPPathPattern(r.Context()); ok { | ||
| span := oteltrace.SpanFromContext(r.Context()) | ||
| span.SetName(r.Method + " " + pattern) | ||
| span.SetAttributes(semconv.HTTPRoute(pattern)) | ||
| } | ||
| next(w, r, pathParams) | ||
| } | ||
| } | ||
|
|
||
| // getCustomHeaderMatcher returns a matcher that matches the given header and prefix | ||
| func getCustomHeaderMatcher(prefixes []string, header string) func(string) (string, bool) { | ||
| header = strings.ToLower(header) | ||
|
|
@@ -239,6 +318,7 @@ func (c *cb) initHTTP(ctx context.Context) (*http.Server, error) { | |
| ), | ||
| runtime.WithMarshalerOption("application/proto", pMar), | ||
| runtime.WithMarshalerOption("application/protobuf", pMar), | ||
| runtime.WithMiddlewares(spanRouteMiddleware), | ||
| } | ||
|
|
||
| if c.config.UseJSONBuiltinMarshaller { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -170,6 +170,122 @@ func TestTracingWrapper(t *testing.T) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func TestTracingWrapper_StatusCodes(t *testing.T) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Parallel() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tests := []struct { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| handler http.HandlerFunc | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| wantStatus int | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: "records 200", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| handler: func(w http.ResponseWriter, _ *http.Request) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| w.WriteHeader(http.StatusOK) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| wantStatus: http.StatusOK, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: "records 404", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| handler: func(w http.ResponseWriter, _ *http.Request) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| w.WriteHeader(http.StatusNotFound) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| wantStatus: http.StatusNotFound, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: "records 500", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| handler: func(w http.ResponseWriter, _ *http.Request) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| w.WriteHeader(http.StatusInternalServerError) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| wantStatus: http.StatusInternalServerError, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: "defaults to 200 when WriteHeader not called", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| handler: func(w http.ResponseWriter, _ *http.Request) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fmt.Fprint(w, "ok") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| wantStatus: http.StatusOK, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, tt := range tests { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Run(tt.name, func(t *testing.T) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Parallel() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| wrapped := tracingWrapper(tt.handler) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| req := httptest.NewRequest("GET", "/api/test", nil) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| w := httptest.NewRecorder() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| wrapped.ServeHTTP(w, req) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if w.Code != tt.wantStatus { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Fatalf("expected %d, got %d", tt.wantStatus, w.Code) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
ankurs marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func TestStatusRecorder(t *testing.T) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Parallel() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Run("captures explicit status", func(t *testing.T) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Parallel() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| w := httptest.NewRecorder() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rec.WriteHeader(http.StatusBadGateway) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if rec.status != http.StatusBadGateway { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Fatalf("expected %d, got %d", http.StatusBadGateway, rec.status) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if w.Code != http.StatusBadGateway { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Fatalf("underlying writer expected %d, got %d", http.StatusBadGateway, w.Code) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Run("switching protocols is terminal", func(t *testing.T) { | |
| t.Parallel() | |
| w := httptest.NewRecorder() | |
| rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK} | |
| rec.WriteHeader(http.StatusSwitchingProtocols) | |
| // A later attempt to change the status should be ignored once 101 is written. | |
| rec.WriteHeader(http.StatusBadGateway) | |
| if rec.status != http.StatusSwitchingProtocols { | |
| t.Fatalf("expected recorder status %d, got %d", http.StatusSwitchingProtocols, rec.status) | |
| } | |
| if w.Code != http.StatusSwitchingProtocols { | |
| t.Fatalf("underlying writer expected %d, got %d", http.StatusSwitchingProtocols, w.Code) | |
| } | |
| }) | |
| t.Run("informational status followed by final status", func(t *testing.T) { | |
| t.Parallel() | |
| w := httptest.NewRecorder() | |
| rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK} | |
| // First write a non-101 informational status; this should be skipped. | |
| rec.WriteHeader(102) | |
| // Then write a final status, which should be recorded and sent. | |
| rec.WriteHeader(http.StatusOK) | |
| if rec.status != http.StatusOK { | |
| t.Fatalf("expected recorder status %d after informational, got %d", http.StatusOK, rec.status) | |
| } | |
| if w.Code != http.StatusOK { | |
| t.Fatalf("underlying writer expected %d after informational, got %d", http.StatusOK, w.Code) | |
| } | |
| }) |
Uh oh!
There was an error while loading. Please reload this page.