3

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!

pascal
  • 641
  • 4
  • 12

2 Answers2

3

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)
    })
}
Thomas Broyer
  • 64,353
  • 7
  • 91
  • 164
  • For future readers, if you need to catch errors half-way into rendering a template, or you want to change the handler signature (return an error from the handler, for example) I would totally recommend this approach of wrapping the response writer to add custom logic to it. – ernestoalejo Sep 20 '16 at 16:54
  • Thank you Thomas Broyer, this solution works well. I've implemented a notFoundHandler which just serves my static 404.html file. – pascal Sep 24 '16 at 07:06
1

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)
}
ernestoalejo
  • 853
  • 1
  • 12
  • 23