Comments (6)
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 io.Discard
if the Etag header is set. I'd prefer this was something we documented rather than implemented directly.
from grpc-gateway.
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),
}
from grpc-gateway.
Given the short comings of using a custom handler, would you be open to this functionality being added behind a ServerMuxOption?
from grpc-gateway.
What the change looks like when put behind an option joshgarnett@d1499d3
from grpc-gateway.
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),
}
from grpc-gateway.
Thanks a lot! This would make an excellent addition to our docs pages, perhaps a new page in our operations
or mapping
folders: https://github.com/grpc-ecosystem/grpc-gateway/tree/main/docs/docs?
from grpc-gateway.
Related Issues (20)
- WriteHeader is not called on 200s HOT 1
- How to set cookie to http response header? Why it doesn't work. HOT 2
- Adding custom mux endpoint to openapi output HOT 3
- The API Gateway Vision HOT 2
- Support disabling default response rendering in bazel rule HOT 1
- Example and pattern annotations are not respect for query and path parameters HOT 8
- FieldMask as query param is not converted to snake case HOT 1
- an google.protobuf.Any type how to get openapiv2 output? HOT 1
- omit-enum-default-value Generates Invalid Spec for Enums with a Single Value HOT 1
- Content-Length not set in ForwardResponseMessage HOT 5
- Log level for errors is "info" HOT 5
- On `context deadline exceeded`, response headers aren't set HOT 3
- TestOutgoingTrailerMatcher is non-deterministic
- How to remove @type or type_url after marshal google.protobuf.Any? HOT 3
- Feat: add http handler middleware HOT 1
- `X-Forwarded-For` header is not handled correctly HOT 2
- README.md: link to Wiki of "tool dependency" has been moved to go.dev HOT 1
- undefined: grpc.NewClient HOT 1
- proposal: Use version info from runtime/debug:BuildInfo when installed by go install HOT 2
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from grpc-gateway.