Skip to content

proposal: net/http: expose status code via optional interface on http.ResponseWriter #70662

Not planned
@marnixbouhuis

Description

@marnixbouhuis

Proposal Details

Expose status code of http response

In this proposal I want to discuss the possibility of exposing the http status code of an HTTP response of an HTTP server.
This is mostly useful for logging and telemetry purposes.

Current situation

Currently, to capture the status code of a request you have to something like this:

type statusRecorder struct {
	writer            http.ResponseWriter
	writeHeaderCalled bool

	StatusCode int
}

var _ http.ResponseWriter = &statusRecorder{}

func (s *statusRecorder) Header() http.Header {
	return s.writer.Header()
}

func (s *statusRecorder) Write(data []byte) (int, error) {
	if !s.writeHeaderCalled {
		// Replicate behaviour from http.ResponseWriter.
		// When Write() is called before WriteHeader(), a 200 OK is returned.
		s.WriteHeader(http.StatusOK)
	}
	return s.writer.Write(data)
}

func (s *statusRecorder) WriteHeader(statusCode int) {
	s.writeHeaderCalled = true
	s.StatusCode = statusCode
	s.writer.WriteHeader(statusCode)
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /", func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
	})

	_ = http.Server{
		Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			st := &statusRecorder{writer: w}
			mux.ServeHTTP(st, r)
			fmt.Printf("Request completed with code %d\n", st.StatusCode)
		}),
	}
}

This works but has a major issue in which it does not expose any optional interfaces implemented by the internal http.response struct.
Things like http.Flusher, http.Hijacker, etc... are lost.

We could implement these extra optional interfaces in the statusRecorder described above but what if the original http.ResponseWriter did not implement this interface?
Code using the wrapped request might incorrectly think that extra functionality is available even though it isn't.

My proposal

Since we can not wrap the http.ResponseWriter without losing these optional interfaces the only real option that we have left is to add another optional interface.

// Ignore function / interface names for now, feedback on this is much appreciated.
type StatusExposer interface {
	StatusCode() int
}

The http.request struct would then implement this extra interface. This would allow you to do the following:

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /", func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
	})

	_ = http.Server{
		Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			// We are not wrapping w, so we do not lose optional interfaces implemented by w.
			mux.ServeHTTP(w, r)

			var code int
			if st, ok := w.(http.StatusExposer); ok {
				code = st.StatusCode()
			}
			fmt.Printf("Request completed with code %d\n", code)
		}),
	}
}

Most structs inside net/http that implement http.ResponseWriter already have easy access to the returned HTTP status code or require minimal changes to save this.
An example implementation of the StatusCode() function could be as simple as adding:

// in src/net/http/server.go
func (w *response) StatusCode() int {
	return w.status
}

The thing that might be a bit tricky with implementation is how to deal with websockets, etc... but I think this is easily handled by tests.

Activity

added this to the Proposal milestone on Dec 3, 2024
marnixbouhuis

marnixbouhuis commented on Dec 3, 2024

@marnixbouhuis
Author

Looks like a similar approach was taken (but abandoned in 2017) in: https://go-review.googlesource.com/c/go/+/36647#related-content

The original author mentions trying a different approach via httptrace but as far as I know the current implementation of httptrace does not provide the response status code. So I think this proposal is still relevant.

seankhliao

seankhliao commented on Dec 3, 2024

@seankhliao
Member

we still recommend wrapping. See ResponseController and the Unwrap method https://pkg.go.dev/net/http#NewResponseController

marnixbouhuis

marnixbouhuis commented on Dec 3, 2024

@marnixbouhuis
Author

Thanks for the response. In the codebase that I'm currently working on I still see a lot of code that depends on direct interface checks but if wrapping is the recommended way of doing this via an Unwrap method then I think migrating the legacy code is the way forward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @gopherbot@marnixbouhuis@seankhliao@gabyhelp

        Issue actions

          proposal: net/http: expose status code via optional interface on http.ResponseWriter · Issue #70662 · golang/go