如何在Golang中使用http.FileServer处理GAE上的404错误

I am using Google App Engine to serve my (semi-)static website generated with Hugo. I have a directory "public" where all the HTML files are stored and are to be served. I also have some server-side scripts for the contact form handling for example. The app.yaml file looks like this.

// app.yaml
runtime: go
api_version: go1

handlers:
- url: /.*
  script: _go_app
  secure: always

And the simplified main.go file looks like this

// main.go
package main

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

  "appengine"
  "appengine/urlfetch"   
)

func init() {

  fileHandler := http.FileServer(http.Dir("public"))
  http.Handle("/", fileHandler)

  http.HandleFunc("/contactus/", HandleContactus)
}

This works perfectly well and serves the html files. However, I am looking at a solution to handle the cases where the pages are not found and the response is 404 Not Found for example (or any other server error).

My thought was to create a custom handler which can be passed in the http.Handle("/", myCustomHandler) and would handle the server response and would redirect to a custom 404.html or the like if necessary. I am new to Go and can't seem to figure out how this should be implemented. I have also looked at the Gorilla Mux, but would prefer (if possible) not to use external libraries to keep it simple.

Based on this post, I have tried the following

package main

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

  "appengine"
  "appengine/urlfetch"   
)

func StaticSiteHandler(h http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){

    h.ServeHTTP(w, r)
  })
}


func init() {

  fileHandler := http.FileServer(http.Dir("public"))
  http.Handle("/", StaticSiteHandler(fileHandler))

  http.HandleFunc("/contactus/", HandleContactus)
}

This solution works in the sense that it also does serve my HTML pages, however I still can't figure out how to handle the server response codes.

Any help would be highly appreciated. Thanks!

To keep the middleware decoupled from the http.FileServer, as you're wrapping it, you can pass a specific implementation of http.ResponseWriter that will:

  1. accumulate headers, in case they'd need to be ditched away (if WriteHeader is called with a 404)
  2. if WriteHeader is called with a 404:
    1. dismiss accumulated headers
    2. send a custom 404
    3. ignore calls with Write from the wrapped handler
  3. if WriteHeader is not called, or called with a non-404, then:
    1. emit accumulated headers to the real ResponseWriter
    2. route the WriteHeader and Write calls to the real ResponseWriter
    type notFoundInterceptorWriter struct {
    rw              http.ResponseWriter // set to nil to signal a 404 has been intercepted
    h               http.Header         // set to nil to signal headers have been emitted
    notFoundHandler http.Handler
    r               *http.Request
}

func (rw *notFoundInterceptorWriter) Header() http.Header {
    if rw.h == nil && rw.rw != nil {
        return rw.rw.Header()
    }
    return rw.h
}

func (rw *notFoundInterceptorWriter) WriteHeader(status int) {
    if status == http.StatusNotFound {
        rw.notFoundHandler.ServeHTTP(rw.rw, rw.r)
        rw.rw = nil
    } else {
        for k, vs := range rw.h {
            for _, v := range vs {
                rw.rw.Header().Add(k, v)
            }
        }
        rw.rw.WriteHeader(status)
    }
    rw.h = nil
}

func (rw *notFoundInterceptorWriter) Write(b []byte) (int, error) {
    if rw.rw != nil {
        return rw.rw.Write(b)
    }
    // ignore, so do as if everything was written OK
    return len(b), nil
}

func StaticSiteHandler(h, notFoundHandler http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w = &notFoundInterceptorWriter{
            rw:              w,
            h:               make(http.Header),
            notFoundHandler: notFoundHandler,
            r:               r,
        }
        h.ServeHTTP(w, r)
    })
}

You can stat the file before serving it to see if it exists. Adapt the 404 handler as needed (emit a template, etc.)

package main

import ( 
  "net/http"
  "path"
  "os"
)

func init() {
    http.Handle("/", staticHandler)
}

func error404Handler(w http.ResponseWriter, r *http.Request) {
    http.Error(w, "404 not found", http.StatusNotFound)
}

func staticHandler(w http.ResponseWriter, r *http.Request) {
    name := path.Clean(r.URL.Path)
    if _, err := os.Stat(name); err != nil {
        if os.IsNotExist(err) {
            error404Handler(w, r)
            return
        }

        http.Error(w, "internal error", http.StatusInternalServerError)
        return
    }

    return http.ServeFile(w, r, name)
}