3

My original question here was flagged as a duplicate of this question. I had no luck implementing it and suspect my problem is misunderstood, so with my question closed, I'm starting fresh with a more specific question.

I'm trying to set a cookie based on a response header from within middleware in request that is reverse proxied.

Here's the workflow:

  • User requests http://example.com/foo/bar
  • Go app uses ReverseProxy to proxy that request to http://baz.com
  • baz.com sets a response header X-FOO
  • Go app modifies response by setting a MYAPPFOO cookie with the value of the X-FOO response header
  • The cookie is written to the user's browser

It was suggested that a custom http.ResponseWriter will work, but after trying and searching for more information, it is not clear how to approach this.

Since I'm failing to grasp the concept of a custom ResponseWriter for my use case, I'll post code that demonstrates more precisely what I was trying to do at the point I got stuck:

package main

import (

    "github.com/gorilla/mux"
    "log"
    "net/http"
    "net/http/httputil"
    "net/url"
)

func setCookie(w http.ResponseWriter, name string, value string) {
    ...
    http.SetCookie(w, &cookie)
}

func handler(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

        // setCookie() works here
        // but I cannot access w.Header().Get("X-FOO")

        next.ServeHTTP(w, r)

        // I can access w.Header().Get("X-FOO") here
        // but setCookie() does not cookie the user's browser

        // If I could do it all in one place, this is what I would do:
        if r.Method == "POST" && r.URL.String() == "/login" {
            foo := w.Header().Get("X-FOO")
            setCookie(w, "MYAPPFOO", foo)

        }
    })
}

func main() {

    r := mux.NewRouter()
    r.Use(handler)
    proxy := httputil.NewSingleHostReverseProxy("https://baz.example.com/")
    r.PathPrefix("/").Handler(proxy)
    log.Fatal(http.ListenAndServe(":9001", r))
}

As a side note, I was able to make this work with ReverseProxy.ModifyResponse as recommended in the comments of my last question but I'd really like to achieve this with middleware to keep the code that dynamically creates proxies from config clean. (not in the example code)

Coder1
  • 13,139
  • 15
  • 59
  • 89
  • You do understand why `setCookie` works before calling `next.ServeHTTP` but not after? – mkopriva Nov 09 '19 at 13:31
  • Yes. I'm just highlighting my conundrum that I can't set the cookie where I can fetch the header I need, and vice versa. – Coder1 Nov 09 '19 at 13:47
  • 1
    Ok, so you know that once you send a response to the client, which I presume is what `next.ServeHTTP(w, r)` does, you cannot *after* that send additional headers (apart from trailers but there Set-Cookie is not allowed I believe). So what you want to do is to write the Set-Cookie header before you write the response. The only way I can think of, that would best match your requirement, *is* to use a custom response writer, one that *postpones* the writing of the response until after your done. Something like this: https://play.golang.com/p/bGOJ9He5pcW (haven't tested it but you get the gist). – mkopriva Nov 09 '19 at 14:04
  • 1
    Basically what you have to do is to write the Header *before* you write the Body (or StatusCode) of a response, it's in the name. How you do that is up to you but the customer response writer is certainly one way to get it done. – mkopriva Nov 09 '19 at 14:09
  • 1
    *"my conundrum that I can't set the cookie where I can fetch the header"* Keep in mind that you *can* set the cookie to the `Header` value just as you can read from the `Header` value, it's just that anything you set to the `Header` *after* the response was sent over HTTP will not magically appear to the client in that already-sent response. This is also explicitly mentioned in the [docs](https://golang.org/pkg/net/http/#ResponseWriter): `Changing the header map after a call to WriteHeader (or Write) has no effect unless the modified headers are trailers.` – mkopriva Nov 09 '19 at 14:18
  • @mkopriva I understand now. Was up all night trying to sort that out. If you add it as an answer here I'll accept. I am grateful, thanks. – Coder1 Nov 09 '19 at 15:05
  • Given that the question is regarding reverse proxy, consider using [ReverseProxy.ModifyResponse](https://godoc.org/net/http/httputil#ReverseProxy.ModifyResponse) to set the cookie. –  Nov 09 '19 at 17:47

1 Answers1

5

From the documentation on http.ResponseWriter methods: (emphasis added)

  • Header() http.Header:

    Changing the header map after a call to WriteHeader (or Write) has no effect unless the modified headers are trailers.

  • WriteHeader(statusCode int):

    WriteHeader sends an HTTP response header with the provided status code.

  • Write([]byte) (int, error):

    If WriteHeader has not yet been called, Write calls WriteHeader(http.StatusOK) before writing the data.

This should highlight the reason why, you can't set a cookie after the next.ServeHTTP(w, r) call, which is that one of the handlers in the middleware chain executed by that call is calling either WriteHeader or Write directly or indirectly.

So to be able set the cookie after the next.ServeHTTP(w, r) call you need to make sure that none of the handlers in the middleware chain calls WriteHeader or Write on the original http.ResponseWriter instance. One way to do this is to wrap the original instance in a custom http.ResponseWriter implementation that will postpone the writing of the response until after you're done with setting the cookie.


For example something like this:

type responsewriter struct {
    w    http.ResponseWriter
    buf  bytes.Buffer
    code int
}

func (rw *responsewriter) Header() http.Header {
    return rw.w.Header()
}

func (rw *responsewriter) WriteHeader(statusCode int) {
    rw.code = statusCode
}

func (rw *responsewriter) Write(data []byte) (int, error) {
    return rw.buf.Write(data)
}

func (rw *responsewriter) Done() (int64, error) {
    if rw.code > 0 {
        rw.w.WriteHeader(rw.code)
    }
    return io.Copy(rw.w, &rw.buf)
}

And you would use it like this in your middleware:

func handler(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        rw := &responsewriter{w: w}
        next.ServeHTTP(rw, r)

        if r.Method == "POST" && r.URL.String() == "/login" {
            foo := rw.Header().Get("X-FOO")
            setCookie(rw, "MYAPPFOO", foo)
        }

        if _, err := rw.Done(); err != nil {
            log.Println(err)
        }
    })
}
mkopriva
  • 35,176
  • 4
  • 57
  • 71