Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inability to use '/' for static files #75

Closed
cachafla opened this issue Jul 17, 2014 · 36 comments
Closed

Inability to use '/' for static files #75

cachafla opened this issue Jul 17, 2014 · 36 comments

Comments

@cachafla
Copy link

Hi,

I'm very sad to find out that gin doesn't allow you to have '/' as your top level route for static files. Almost every single web framework out there assumes that the static files you have inside your project are going to be served from $static_dir and they will be mapped as HTTP routes from '/'. An example from express.js does this:

server.use(express.static(__dirname + '/public'));

Doing that maps "/index.html" to "public/index.html". Expected behavior. Any other frameworks like Rails or Martini have this convention as well. Basically it means that if you have the following files inside a "./public" directory:

--- public/
 |
 |--- index.html
 |--- css/
 |      |--- app.css

Files will be accessible with HTTP requests like $server_url/index.html or $server_url/css/app.css. Gin disallows you to do this (although the problem lies in httprouter), and forces you to have a separate (non-top-level) route for public/static files like:

router.Static("/static", "/var/www")

So, if I use this:

router.Static("/", "/var/www")

I get this panic:

panic: wildcard route conflicts with existing children

goroutine 16 [running]:
runtime.panic(0x2d5da0, 0xc2080013a0)
    /usr/local/go/src/pkg/runtime/panic.c:279 +0xf5
github.com/julienschmidt/httprouter.(*node).insertChild(0xc208004360, 0xc208001302, 0xc208040de1, 0x10, 0xc208040e20)
    /Users/cachafla/Code/Go/go/src/github.com/julienschmidt/httprouter/tree.go:201 +0x11e
github.com/julienschmidt/httprouter.(*node).addRoute(0xc208004360, 0xc208040de1, 0x10, 0xc208040e20)
    /Users/cachafla/Code/Go/go/src/github.com/julienschmidt/httprouter/tree.go:172 +0x952
github.com/julienschmidt/httprouter.(*Router).Handle(0xc208040c60, 0x3ce8b0, 0x3, 0xc208040de0, 0x11, 0xc208040e20)
    /Users/cachafla/Code/Go/go/src/github.com/julienschmidt/httprouter/router.go:205 +0x186
github.com/gin-gonic/gin.(*RouterGroup).Handle(0xc208070340, 0x3ce8b0, 0x3, 0xc208040de0, 0x11, 0xc2080380a0, 0x1, 0x1)
    /Users/cachafla/Code/Go/go/src/github.com/gin-gonic/gin/gin.go:223 +0x477
github.com/gin-gonic/gin.(*RouterGroup).GET(0xc208070340, 0xc208040d40, 0x11, 0xc2080380a0, 0x1, 0x1)
    /Users/cachafla/Code/Go/go/src/github.com/gin-gonic/gin/gin.go:233 +0x6d
github.com/gin-gonic/gin.(*RouterGroup).Static(0xc208070340, 0xc208040d40, 0x11, 0x3eed10, 0x8)
    /Users/cachafla/Code/Go/go/src/github.com/gin-gonic/gin/gin.go:276 +0x252

It might seem silly, but this is actually an incorrect behavior for defining a static directory to be served by the HTTP server. The router should be able to handle multiple matching routes, as almost any other HTTP library out there In my case, I really need my index.html to be /index.html and not /static/index.html.

In any case, I understand that this comes from a predefined httprouter behavior and I acknowledge that I could use a prefix and "deal with it", but I would prefer if this worked as it should. If somebody has any tips on how to workaround this issue I would really appreciate it, thanks!

@manucorporat
Copy link
Contributor

Of course this is possible in Gin. Both martini and express are doing it using middlewares.

  • The built-in Static() method in Gin is useful when using groups, for example you can protect the static/ folder with basic HTTP authorization.

We could create a Static middleware. I am working on that

@manucorporat
Copy link
Contributor

I just created an static middleware:
https://github.com/gin-gonic/contrib/tree/master/static

Use it like martini:

r := gin.Default()
r.Use(static.Serve("/public")) // static files have higher priority over dynamic routes
r.NotFound(static.Serve("/public")) // when no route is found, serving static files is tried.

@cachafla
Copy link
Author

Hi @manucorporat. It looks nice. I had found similar solutions using the NotFound handler too, but it's nice to see that this would be a handler coming directly out of the contrib repository. Personally I would prefer if this didn't have to be done through a NotFound handler but I understand how httprouter prevents that, it's OK :)

I will try your code, thanks a lot for the help!

@manucorporat
Copy link
Contributor

Do not worry about using NotFound(). It is the most effective way.
Martini uses the same approach, using Use() in martini is not the most effective way, because for every single request martini have to check if the file exist! I guess you agree that it doesn't make sense! right?

The flow is:

Using Use() in martini and Gin (and probably in express) as well:

----REQUEST----> (check file for path)  ---- exist ---> serve content
                                        --- no exist ---> your normal handler

using NotFound():

----REQUEST----> your normal handler
           ----> not handler ---> check file and serve content

@cachafla
Copy link
Author

Hehe yeah, I'm not a big fan of Martini anyway.

@cachafla
Copy link
Author

Again, thanks for this!

@manucorporat
Copy link
Contributor

I am going to rename NotFound() to NoRoute(). Just to make it clear that that handlers are not related with 404. 404 is up to you ;)

@cachafla
Copy link
Author

Just a thought: it would be nice to be handle pass handlers for specific error codes, so you can provide custom 404, 500, etc. handlers for your API.

@manucorporat
Copy link
Contributor

You do not need that! You can create a middleware like this:

func Handle500(c *gin.Context) {
   c.Next()
   if c.Writer.Status() == 500 {
       // do something
   }

NoRoute() exists because it's the only way to catch a non-handled route!
like for example, for /css/image.png there is not a defined route, so: NoRoute() is called instead!
Then you can server the image, and it would not be a 404 at all.

@muei
Copy link

muei commented Jul 31, 2014

If I want add more static directories, how to ?

@00imvj00
Copy link

can you provide any example , bcz i am not getting how to setup it for all my css , js , images , html files.

@matejkramny
Copy link

👍 i'd love an example too.

@akashshinde
Copy link

I am also having some issue getting static html with css,js files,If possible please provide example.

@rogierlommers
Copy link
Contributor

I'm using this, working great! Please note that you'll have to import github.com/gin-gonic/contrib/static

r := gin.Default()
r.Use(static.Serve("/", static.LocalFile("html", false)))

@regexb
Copy link

regexb commented Feb 12, 2015

Perfect example @rogierlommers! Thanks!

@geekgonecrazy
Copy link

This would be awesome if in the readme. Glad I took a look here before giving up. Thanks for the middleware!

@rogierlommers
Copy link
Contributor

Have created pull request to update readme: #221

@geekgonecrazy
Copy link

@rogierlommers perfect! Much appreciated. 😄

@kpbird
Copy link

kpbird commented Dec 4, 2015

I am having same issue

I have public directory in which all html, css, js reside.

Dir structure

project folder
|
-- public
|
---- index.html
main.go


Following code don't serve anything (404 error)
router := gin.Default()
router.Use(static.ServeRoot("/","public"))
router.Run(":8080")


url : http://localhost:8080 -> 404 error
url : http://localhost:8080/index.html -> 404 error

Can you please help me..

@drKnoxy
Copy link

drKnoxy commented Jun 3, 2016

👍 thank you for making this so easy.

For anyone else that wants to set up a static server and api on the same url here is what I ended up with:

r := gin.Default()

r.Use(static.Serve("/", static.LocalFile("./public", true)))

api := r.Group("/api")
{
    api.GET("/events", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
}

r.Run()
// file system
public/
  index.html
  css.css
main.go

@ghost
Copy link

ghost commented Oct 22, 2016

This drove me crazy. For anyone else who comes here seeking the same answer: you can use the built-in router.Static() method (not the contrib package), but the argument for the root path of the static directory needs to be either an absolute path, or a relative path that's relative to the current working directory, not the binary and not the Go source files. Drove me nuts.

@terrywarwar
Copy link

@winduptoy can you post some code of how you used it.

@ghost
Copy link

ghost commented Nov 16, 2016

@terrywarwar

Absolute path:

router.StaticFile("/", "/home/terry/project/src/static/index.html")

Relative path:

router.StaticFile("/", "./src/static/index.html")

In the example above, the relative path depends on your current working directory being /home/terry/project, even though your compiled executable may exist in /home/terry/project/bin, so you'd have to execute the following:

$ cd /home/terry/project
$ ./bin/my-server

@terrywarwar
Copy link

thanks @winduptoy

@caixuguang123
Copy link

thankyou~

@treeder
Copy link

treeder commented Feb 19, 2017

@manucorporat I'm looking at the static contrib library (https://github.com/gin-contrib/static) and wondering why it doesn't use NoRoute() instead of Use() since as you said above, NoRoute is the better option for this.

@kasora
Copy link

kasora commented Jan 17, 2018

If you just want to send a HTML file when NoRoute. (It's normally in SPA)
You don't need the static package.

For example, you want to send ${workspaceRoot}/static/index.html for default file.

router.NoRoute(func(c *gin.Context) {
	c.File("workspaceRoot/static/index.html")
})

May be you're looking for it?

@yingshaoxo
Copy link

yingshaoxo commented May 1, 2019

thank you for making this so easy.

For anyone else that wants to set up a static server and api on the same url here is what I ended up with:

r := gin.Default()

r.Use(static.Serve("/", static.LocalFile("./public", true)))

api := r.Group("/api")
{
    api.GET("/events", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
}

r.Run()
// file system
public/
  index.html
  css.css
main.go

But the above codes will not work when you want to package all static files to a single binary file:
https://github.com/gobuffalo/packr/tree/master/v2#usage-with-http

The github.com/gin-gonic/contrib/static model does not provide such a function for taking HTTP FileSystem parameter.

@Ayanrocks
Copy link

Ayanrocks commented Nov 4, 2019

import 	"github.com/gin-gonic/contrib/static"
...
r := gin.Default()
r.Use(static.Serve("/", static.LocalFile("/view", false)))

this is throwing the following error

cannot use static.Serve("/", static.LocalFile("/view", true)) (value of type gin.HandlerFunc) as gin.HandlerFunc value in argument to r.Use

Any idea?

@Ayanrocks
Copy link

import 	"github.com/gin-gonic/contrib/static"
...
r := gin.Default()
r.Use(static.Serve("/", static.LocalFile("/view", false)))

this is throwing the following error

cannot use static.Serve("/", static.LocalFile("/view", true)) (value of type gin.HandlerFunc) as gin.HandlerFunc value in argument to r.Use

Any idea?

Fixed the error. Turns out I was using glide so there was a dependency type mismatch

@akerdi
Copy link

akerdi commented Nov 28, 2019

g.NoRoute(Middlewares.ReturnPublic())

middleware.go
// ReturnPublic send fontend to client when method == GET
func ReturnPublic() gin.HandlerFunc {
	return func(context *gin.Context) {
		method := context.Request.Method
		if method == "GET" {
			context.File("./public")
		} else {
			context.Next()
		}
	}
}

this works well

@kmahyyg
Copy link

kmahyyg commented Feb 1, 2020

My route is : /:itemid then I want to serve my index and faq page on /index.html /faq.html which my static files are located at /static folder?

How should I do?

@GTANAdam
Copy link

GTANAdam commented Mar 24, 2020

thank you for making this so easy.
For anyone else that wants to set up a static server and api on the same url here is what I ended up with:

r := gin.Default()

r.Use(static.Serve("/", static.LocalFile("./public", true)))

api := r.Group("/api")
{
    api.GET("/events", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
}

r.Run()
// file system
public/
  index.html
  css.css
main.go

But the above codes will not work when you want to package all static files to a single binary file:
https://github.com/gobuffalo/packr/tree/master/v2#usage-with-http

The github.com/gin-gonic/contrib/static model does not provide such a function for taking HTTP FileSystem parameter.

For packr2, it can be fixed with the following example:

r := gin.Default()

myFiles := packr.New("my files", "./www")

w.Use(StaticServe("/", myFiles))

api := r.Group("/api")
{
    api.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
}

r.Run()

middleware:

// StaticServe serves the data
func StaticServe(urlPrefix string, fs *packr.Box) gin.HandlerFunc {
	fileserver := http.FileServer(fs)
	if urlPrefix != "" {
		fileserver = http.StripPrefix(urlPrefix, fileserver)
	}
	return func(c *gin.Context) {
		if fs.Has(c.Request.URL.Path) {
			fileserver.ServeHTTP(c.Writer, c.Request)
			c.Abort()
		}
	}
}

EDIT: Forgot to mention that this does not work for folders, as in navigating to "/" will not return the index.html file, so in order to fix this:

  • We add the add the local 'exists' function
func exists(fs *packr.Box, prefix string, filepath string) bool {
	if p := strings.TrimPrefix(filepath, prefix); len(p) < len(filepath) {
		name := path.Join("/", p)
		if fs.HasDir(name) {
			index := path.Join(name, "index.html")
			if !fs.Has(index) {
				return false
			}
		} else if !fs.Has(name) {
			return false
		}

		return true
	}
	return false
}
  • Then we replace in the middleware function:
if fs.Has(c.Request.URL.Path) {

with

if exists(fs, urlPrefix, c.Request.URL.Path) {

@rickyseezy
Copy link

For those who are looking for an updated answer

r.StaticFS("/public", http.Dir("uploads")) r.Run(":8080")

@kishvanchee
Copy link

Can confirm what @rickyseezy suggested works. My use case is serving the swagger ui along with my APIs. The blog post combined with the above comment helped.

@soulteary
Copy link

I made a simple update of the community's static middleware. If anyone has similar needs, maybe you can use it.

Supports both local files and Go Embed (you can create a single executable file without having to deal with file packaging)

project: https://github.com/soulteary/gin-static

download the middleware:

go get github.com/soulteary/gin-static

use local files:

package main

import (
	["log"](https://pkg.go.dev/log)

	static "github.com/soulteary/gin-static"
	https://github.com/gin-gonic/gin
)

func main() {
	r := gin.Default()

	// if Allow DirectoryIndex
	// r.Use(static.Serve("/", static.LocalFile("./public", true)))
	// set prefix
	// r.Use(static.Serve("/static", static.LocalFile("./public", true)))

	r.Use(static.Serve("/", static.LocalFile("./public", false)))

	r.GET("/ping", func(c *gin.Context) {
		c.String(200, "test")
	})

	// Listen and Server in 0.0.0.0:8080
	if err := r.Run(":8080"); err != nil {
		log.Fatal(err)
	}
}

use embed files:

package main

import (
	["embed"](https://pkg.go.dev/embed)
	["fmt"](https://pkg.go.dev/fmt)
	["net/http"](https://pkg.go.dev/net/http)

	https://github.com/gin-gonic/gin
)

//go:embed public
var EmbedFS embed.FS

func main() {
	r := gin.Default()

	// method 1: use as Gin Router
	// trim embedfs path `public/page`, and use it as url path `/`
	// r.GET("/", static.ServeEmbed("public/page", EmbedFS))

	// method 2: use as middleware
	// trim embedfs path `public/page`, the embedfs path start with `/`
	// r.Use(static.ServeEmbed("public/page", EmbedFS))

	// method 2.1: use as middleware
	// trim embedfs path `public/page`, the embedfs path start with `/public/page`
	// r.Use(static.ServeEmbed("", EmbedFS))

	// method 3: use as manual
	// trim embedfs path `public/page`, the embedfs path start with `/public/page`
	// staticFiles, err := static.EmbedFolder(EmbedFS, "public/page")
	// if err != nil {
	// 	log.Fatalln("initialization of embed folder failed:", err)
	// } else {
	// 	r.Use(static.Serve("/", staticFiles))
	// }

	r.GET("/ping", func(c *gin.Context) {
		c.String(200, "test")
	})

	r.NoRoute(func(c *gin.Context) {
		fmt.Printf("%s doesn't exists, redirect on /\n", c.Request.URL.Path)
		c.Redirect(http.StatusMovedPermanently, "/")
	})

	// Listen and Server in 0.0.0.0:8080
	r.Run(":8080")
}

or both use local and embed file, and as those files as fallback (you can overwrite the routes):

if debugMode {
	r.Use(static.Serve("/", static.LocalFile("public", false)))
} else {
	r.NoRoute(
		// your code ...
		func(c *gin.Context) {
			if c.Request.URL.Path == "/somewhere/" {
				c.Data(http.StatusOK, "text/html; charset=utf-8", []byte("custom as you like"))
				c.Abort()
			}
		},
		static.ServeEmbed("public", EmbedFS),
	)
	// no need to block some request path before request static files
	// r.NoRoute(static.ServeEmbed("public", EmbedFS))
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests