【问题标题】:Custom 404 with Gorilla Mux and std http.FileServer带有 Gorilla Mux 和 std http.FileServer 的自定义 404
【发布时间】:2023-12-09 21:54:01
【问题描述】:

我有以下代码,一切正常。

var view404 = template.Must(template.ParseFiles("views/404.html"))

func NotFound(w http.ResponseWriter, r *http.Request) {
  w.WriteHeader(404)
  err := view404.Execute(w, nil)
  check(err)
}

func main() {
  router := mux.NewRouter()
  router.StrictSlash(true)
  router.NotFoundHandler = http.HandlerFunc(NotFound)
  router.Handle("/", IndexHandler).Methods("GET")
  router.PathPrefix("/public/").Handler(http.StripPrefix("/public/", http.FileServer(http.Dir("public"))))
  http.Handle("/", router)
  http.ListenAndServe(":8000", nil)
}

/cannot/find 之类的路由的请求会显示我的自定义 404 模板。我的/public/ 目录中的所有静态文件也都得到了正确的处理。

我在处理不存在的静态文件并为它们显示我的自定义 NotFound 处理程序时遇到问题。对/public/cannot/find 的请求调用标准的 http.NotFoundHandler 并回复

404 页面未找到

如何为普通路由和静态文件使用相同的自定义 NotFoundHandler?


更新

我最终按照@Dewy Broto 的建议通过包装http.ServeFile 来实现我自己的FileHandler

type FileHandler struct {
  Path string
}

func (f FileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  prefix := "public"
  http.ServeFile(w, r, path.Join(prefix, f.Path))
}

// ...

router.Handle("/css/styles.css", FileHandler{"/css/styles.css"}).Methods("GET")

现在我的NotFound 处理程序会捕获所有丢失的路由甚至丢失的文件。

【问题讨论】:

  • 呃。现在,您将需要一个处理程序来处理您要服务的每个文件。图片、javascript 等...我更喜欢他的钩子解决方案,但即使是这样一个简单的目标,也需要大量代码。

标签: go


【解决方案1】:

FileServer 正在生成 404 响应。 FileServer 处理由 mux 传递给它的所有请求,包括对丢失文件的请求。有几种方法可以通过自定义 404 页面提供静态文件:

  • 使用ServeContent 编写您自己的文件处理程序。此处理程序可以以您想要的任何方式生成错误响应。如果不生成索引页面,代码不会很多。
  • 用另一个处理程序包装FileServer 处理程序,该处理程序挂钩传递给 FileHandler 的ResponseWriter。当调用 WriteHeader(404) 时,钩子会写入不同的主体。
  • 向多路复用器注册每个静态资源,以便多路复用器中的包罗万象处理未找到的错误。这种方法需要对 ServeFile 进行简单的包装。

这是第二种方法中描述的包装器的草图:

type hookedResponseWriter struct {
    http.ResponseWriter
    ignore bool
}

func (hrw *hookedResponseWriter) WriteHeader(status int) {
    hrw.ResponseWriter.WriteHeader(status)
    if status == 404 {
        hrw.ignore = true
        // Write custom error here to hrw.ResponseWriter
    }
}

func (hrw *hookedResponseWriter) Write(p []byte) (int, error) {
    if hrw.ignore {
        return len(p), nil
    }
    return hrw.ResponseWriter.Write(p)
}

type NotFoundHook struct {
    h http.Handler
}

func (nfh NotFoundHook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    nfh.h.ServeHTTP(&hookedResponseWriter{ResponseWriter: w}, r)
}

通过包装文件服务器来使用钩子:

 router.PathPrefix("/public/").Handler(NotFoundHook{http.StripPrefix("/public/", http.FileServer(http.Dir("public")))})

这个简单钩子的一个警告是它会阻止服务器中的优化,以便从文件复制到套接字。

【讨论】:

  • 方法 2 会很棒。
  • 谢谢!我选择了方法 3,因为它让我可以最大程度地控制所提供的服务。我没有那么多静态文件,它们可能会弄乱我的代码。请参阅上面的更新。
  • 绝妙的方法——这不仅仅适用于Gorilla的项目