Giter Club home page Giter Club logo

Comments (6)

johanbrandhorst avatar johanbrandhorst commented on June 2, 2024

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.

joshgarnett avatar joshgarnett commented on June 2, 2024

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.

joshgarnett avatar joshgarnett commented on June 2, 2024

Given the short comings of using a custom handler, would you be open to this functionality being added behind a ServerMuxOption?

from grpc-gateway.

joshgarnett avatar joshgarnett commented on June 2, 2024

What the change looks like when put behind an option joshgarnett@d1499d3

from grpc-gateway.

joshgarnett avatar joshgarnett commented on June 2, 2024

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.

johanbrandhorst avatar johanbrandhorst commented on June 2, 2024

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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.