在go中为多个处理程序设置http标头

问题描述:

I'm trying to set an http header for multiple handlers. My first thought was to make a custom write function that would set the header before writing the response like the code sample at the bottom.

However, when I pass a pointer to the http.ResponseWriter and try to access it from my function it tells me that "type *http.ResponseWriter has no Header method".

What is the best way to set headers for multiple handlers, and also why isn't the pointer working the way I want it to?

func HelloServer(w http.ResponseWriter, req *http.Request) {
    type Message struct {
        Name string
        Body string
        Time int64
    }

    m := Message{"Alice", "Hello", 1294706395881547000}

    b, _ := json.Marshal(m)
    WriteJSON(&w, b)
}

func WriteJSON(wr *http.ResponseWriter, rawJSON []byte) {
    *wr.Header().Set("Content-Type", "application/json")

    io.WriteString(*wr, string(rawJSON))
}

func main() {
    http.HandleFunc("/json", HelloServer)

    err := http.ListenAndServe(":9000", nil)
    if err != nil {
    log.Fatal("ListenAndServer: ", err)
    }
}

I'm not sure about the multiple handlers thing, but I do know why the code you wrote is failing. The key is that the line:

*wr.Header().Set("Content-Type", "application/json")

is being interpreted, because of operator precedence, as:

*(wr.Header().Set("Content-Type", "application/json"))

Since wr has the type *http.ResponseWriter, which is a pointer to and interface, rather than the interface itself, this won't work. I assume that you knew that, which is why you did *wr. I assume what you meant to imply to the compiler is:

(*wr).Header().Set("Content-Type", "application/json")

If I'm not mistaken, that should compile and behave properly.

You don't need to use *wr as it already references a pointer.

wr.Header().Set("Content-Type", "application/json") should be sufficient.

If you want to set "global" headers for every request, you can create a function that satisfies http.HandleFunc (go.auth has a good example) and then wrap your handlers like so:

http.HandleFunc("/hello", Defaults(helloHandler))

Also take a look at the net/http documentation, which has further examples.

I wrap my handlers with an error handler which calls my AddSafeHeader function.

I based it on http://golang.org/doc/articles/error_handling.html but it doesn't use ServeHTTP so it works with appstats:

http.Handle("/", appstats.NewHandler(util.ErrorHandler(rootHandler)))

Here:

package httputil

import (
  "appengine"
  "net/http"
  "html/template"
)

func AddSafeHeaders(w http.ResponseWriter) {
  w.Header().Set("X-Content-Type-Options", "nosniff")
  w.Header().Set("X-XSS-Protection", "1; mode=block")
  w.Header().Set("X-Frame-Options", "SAMEORIGIN")
  w.Header().Set("Strict-Transport-Security", "max-age=2592000; includeSubDomains")
}

// Redirect to a fixed URL
type redirectHandler struct {
  url  string
  code int
}

func (rh *redirectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  Redirect(w, r, rh.url, rh.code)
}

func Redirect(w http.ResponseWriter, r *http.Request, urlStr string, code int) {
  AddSafeHeaders(w)
  http.Redirect(w, r, urlStr, code)
}

// RedirectHandler returns a request handler that redirects
// each request it receives to the given url using the given
// status code.
func RedirectHandler(url string, code int) http.Handler {
  return &redirectHandler{url, code}
}

func ErrorHandler(fn func(appengine.Context, http.ResponseWriter, *http.Request)) func(appengine.Context, http.ResponseWriter, *http.Request) {
  return func(c appengine.Context, w http.ResponseWriter, r *http.Request) {
    defer func() {
      if err, ok := recover().(error); ok {
        c.Errorf("%v", err)
        w.WriteHeader(http.StatusInternalServerError)
        errorTemplate.Execute(w, err)
      }
    }()
    AddSafeHeaders(w)
    fn(c, w, r)
  }
}

// Check aborts the current execution if err is non-nil.
func Check(err error) {
  if err != nil {
    panic(err)
  }
}

var errorTemplate = template.Must(template.New("error").Parse(errorTemplateHTML))

const errorTemplateHTML = `
<html>
<head>
        <title>XXX</title>
</head>
<body>
        <h2>An error occurred:</h2>
        <p>{{.}}</p>
</body>
</html>
`

http.ResponseWriter is an interface.

You should probably not be using a pointer to an interface. In net/http/server.go, the unexported response struct is the actual type that implements ResponseWriter when the server calls your handler, and importantly, when the server actually calls the handler's ServeHTTP, it passes a *response. It's already a pointer, but you don't see that because ResonseWriter is an interface. (the response pointer is created here, by (c *conn).readRequest. (The links will likely be for the wrong lines the future, but you should be able to locate them).

That's why the ServeHTTP function required to implement Handler is:

ServeHTTP(w ResponseWriter, r *Request)

i.e. not a pointer to ResponseWriter, as this declaration already permits a pointer to a struct that implements the ResponseWriter interface.

As I am new to Go, I created a minimal contrived example, based on elithrar's answer, which shows how to easily add headers to all your routes / responses. We do so, by creating a function that satisfies the http.HandlerFunc interface, then wraps the route handler functions:

package main

import (
    "encoding/json"
    "log"
    "net/http"


    "github.com/gorilla/mux"
)


// Hello world.
func Hello(w http.ResponseWriter, r *http.Request) {
    json.NewEncoder(w).Encode("Hello World")
}

// HelloTwo world
func HelloTwo(w http.ResponseWriter, r *http.Request) {
    json.NewEncoder(w).Encode("Hello Two World")
}

// JSONHeaders conforms to the http.HandlerFunc interface, and
// adds the Content-Type: application/json header to each response.
func JSONHeaders(handler http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        handler(w, r)
    }
}

func main() {   
    router := mux.NewRouter()
    // Now, instead of calling your handler function directly, pass it into the wrapper function.
    router.HandleFunc("/", JSONHeaders(Hello)).Methods("GET") 
    router.HandleFunc("/hellotwo", JSONHeaders(HelloTwo)).Methods("GET")

    log.Fatal(http.ListenAndServe(":3000", router))
}

Results:

$ go run test.go &
$ curl -i localhost:3000/
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 28 Feb 2019 22:27:04 GMT
Content-Length: 14

"Hello World"