-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
Add support for Etag & If-None-Match headers #4263
Comments
Thanks for your issue 😁. I'm sympathetic to this request, but this is major change in behavior (and role) of the gateway. If you want to avoid writing the body to the client, you can create a custom responseWriter that replaces the real writer with |
I've whipped up an example that appears to work with a custom responseWriter. The main downside is the ForwardResponseOption needs to marshal the message to a byte array. Also, the option doesn't have access to the request, so it can't limit writing etags to GET requests. I know it would be another big change, but it would be really nice if a ForwardResponseOption could have access to the marshaled message and the request. Here is what the code looks like: import (
"context"
"crypto/md5"
"encoding/hex"
"net/http"
"google.golang.org/protobuf/proto"
)
type etagWriter struct {
http.ResponseWriter
wroteHeader bool
ifNoneMatch string
}
func (w *etagWriter) Write(b []byte) (int, error) {
etag := w.Header().Get("Etag")
if !w.wroteHeader && w.ifNoneMatch == etag {
w.ResponseWriter.WriteHeader(http.StatusNotModified)
return 0, nil
} else {
return w.ResponseWriter.Write(b)
}
}
func (w *etagWriter) WriteHeader(code int) {
w.wroteHeader = true
w.ResponseWriter.WriteHeader(code)
}
// Unwrap returns the original http.ResponseWriter. This is necessary
// to expose Flush() and Push() on the underlying response writer.
func (w *etagWriter) Unwrap() http.ResponseWriter {
return w.ResponseWriter
}
// IfNoneMatchHandler wraps an http.Handler and will return a NotModified
// response if the If-None-Match header matches the Etag header.
func IfNoneMatchHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ifNoneMatch := r.Header.Get("If-None-Match")
if ifNoneMatch != "" {
w = &etagWriter{
ResponseWriter: w,
ifNoneMatch: ifNoneMatch,
}
}
h.ServeHTTP(w, r)
})
}
func ForwardResponseWithEtag(_ context.Context, w http.ResponseWriter, m proto.Message) error {
// NOTE: Unfortunately we have to serialize the protobuf
data, err := proto.Marshal(m)
if err != nil {
return err
}
// NOTE: We don't have access to the request, so this can't be limited to just GET methods
if len(data) > 100 {
h := md5.New()
h.Write(data)
etag := hex.EncodeToString(h.Sum(nil))
w.Header().Set("Etag", "\""+etag+"\"")
}
return nil
} Usage code looks like: mux := runtime.NewServeMux(runtime.WithForwardResponseOption(ForwardResponseWithEtag))
// Register generated gateway handlers
s := &http.Server{
Handler: IfNoneMatchHandler(mux),
} |
Given the short comings of using a custom handler, would you be open to this functionality being added behind a ServerMuxOption? |
What the change looks like when put behind an option joshgarnett@d1499d3 |
Alright, I thought through this some more over coffee this morning. I've rewritten the example code so it doesn't suffer from the problems I highlighted. This could be added to the documentation. import (
"crypto/md5"
"encoding/hex"
"fmt"
"net/http"
)
type etagWriter struct {
http.ResponseWriter
wroteHeader bool
ifNoneMatch string
writeEtag bool
minBytes int
}
func (w etagWriter) Write(b []byte) (int, error) {
if w.wroteHeader || !w.writeEtag || len(b) < w.minBytes {
return w.ResponseWriter.Write(b)
}
// Generate the Etag
h := md5.New()
h.Write(b)
etag := fmt.Sprintf("\"%s\"", hex.EncodeToString(h.Sum(nil)))
w.Header().Set("Etag", etag)
if w.ifNoneMatch != "" && w.ifNoneMatch == etag {
w.ResponseWriter.WriteHeader(http.StatusNotModified)
return 0, nil
} else {
return w.ResponseWriter.Write(b)
}
}
func (w etagWriter) WriteHeader(code int) {
// Track if the headers have already been written
w.wroteHeader = true
w.ResponseWriter.WriteHeader(code)
}
// Unwrap returns the original http.ResponseWriter. This is necessary
// to expose Flush() and Push() on the underlying response writer.
func (w etagWriter) Unwrap() http.ResponseWriter {
return w.ResponseWriter
}
// EtagHandler wraps an http.Handler and will write an Etag header to the
// response if the request method is GET and the response size is greater
// than or equal to minBytes. It will also return a NotModified response
// if the If-None-Match header matches the Etag header.
func EtagHandler(h http.Handler, minBytes int) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w = etagWriter{
ResponseWriter: w,
ifNoneMatch: r.Header.Get("If-None-Match"),
writeEtag: r.Method == http.MethodGet,
minBytes: minBytes,
}
h.ServeHTTP(w, r)
})
} Usage code: mux := runtime.NewServeMux()
// Register generated gateway handlers
s := &http.Server{
Handler: EtagHandler(mux, 100),
} |
Thanks a lot! This would make an excellent addition to our docs pages, perhaps a new page in our |
Taken from https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag: "The ETag (or entity tag) HTTP response header is an identifier for a specific version of a resource. It lets caches be more efficient and save bandwidth, as a web server does not need to resend a full response if the content was not changed."
CDNs & Clients can send the Etag in subsequent requests as the If-None-Match header. The server can then skip writing the response if the header matches the Etag of the response. For large responses and clients on poor networks, this can help out a lot.
This can't be implemented with a WithForwardResponseOption, since that doesn't allow you to stop the ForwardResponseMessage method from writing the message to the client.
The text was updated successfully, but these errors were encountered: