Go Gorilla Mux not routing as expected - rest

I am having issues getting the Gorilla Mux library for Go to work. From the documentation I have read and all the debugging I've done, I cannot seem to figure out what the problem is. Here's what I've got for routing:
Folder structure:
project_root
|-- main.go
|-- routes
|-- routes.go
|-- user.go
main.go:
package main
import (
"fmt"
"net/http"
"./routes"
)
func main() {
r := routes.CreateRoutes(http.Dir("./content"))
http.Handle("/", r)
err := http.ListenAndServe(fmt.Sprintf("%s:%d", "localhost", 8000), nil)
if err != nil {
fmt.Println("Error: ", err)
}
}
routes/routes.go
package routes
import (
"net/http"
"github.com/gorilla/mux"
)
func CreateRoutes(staticDir http.FileSystem) *mux.Router {
r := mux.NewRouter()
// Serve static pages (i.e. web app)
r.PathPrefix("/").Handler(http.FileServer(staticDir))
// Serve User Pages
createUserRoutes(r)
return r
}
routes/user.go
package routes
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
)
func createUserRoutes(r *mux.Router) {
user := r.PathPrefix("/user/").Subrouter()
// Create a new user
user.Path("/new").Methods("PUT").HandlerFunc(newUserHandler)
// Remove a user
user.Path("/remove/{username:[a-z][a-z0-9]+}").Methods("DELETE").HandlerFunc(removeUserHandler)
// Update a user
user.Path("update/{username:[a-z][a-z0-9]+").Methods("POST").HandlerFunc(updateUserHandler)
// Get a user (Get user information)
user.Path("/{username:[a-z][a-z0-9]+").Methods("GET").HandlerFunc(getUserHandler)
}
func newUserHandler(resp http.ResponseWriter, req *http.Request) {
// Do something that might cause an error
if err != nil {
fmt.Println(err)
resp.WriteHeader(409)
resp.Write([]byte(err.Error()))
} else {
fmt.Println("Created new user")
resp.WriteHeader(201)
resp.Write([]byte("Created new user"))
}
}
func removeUserHandler(resp http.ResponseWriter, req *http.Request) {
}
func updateUserHandler(resp http.ResponseWriter, req *http.Request) {
}
func getUserHandler(resp http.ResponseWriter, req *http.Request) {
}
Whenever I make a request to root path of the server (i.e. the path that serves the static content), the server responds as intended, with the main page. However, any other calls result in a 404 response (I test requests using cURL). For example, a malformed request to http://localhost:8000/user/new should return a 409, but instead returns a 404. Same if I expect a 201 response.
Everything looks right and I've triple checked it, but I cannot figure out what the issue here is.

Turns out the solution was simple (like it usually is). This line in routes.go
r.PathPrefix("/").Handler(http.FileServer(staticDir))
was causing the unintended routing. When PathPrefix is used, it seems to route all URLs to the first matching prefix (in this case this prefix). This explains why static files were being served, but nothing else works.
The fix is to use the Path function instead. There's a subtle difference as explained in the docs; PathPrefix "matches if the given template is a prefix of the full URL path", whereas Path does not. Hence the line above now looks like this to solve the issue I was having:
r.Path("/").Handler(http.FileServer(staticDir))

Related

How to use an optional query parameter in Go gorilla/mux?

Probably there is already a solution here to my problem, but I couldn't find it anywhere. I tried a bunch of stuff, but nothing worked so far.
I have something like this:
package main
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
)
func HealthCheck(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "Healthy")
// Also print the value of 'foo'
}
func main() {
r := mux.NewRouter()
r.HandleFunc("/health-check", HealthCheck).Methods("GET").Queries("foo", "{foo}").Name("HealthCheck")
r.HandleFunc("/health-check", HealthCheck).Methods("GET")
http.ListenAndServe(":8080", r)
}
What I'm trying to achieve:
curl http://localhost:8080/health-check
Should respond with: Healthy <foo> ( -> the default value of foo)
And also the following:
curl http://localhost:8080/health-check?foo=bar
Should respond with: Healthy bar
One solution if to simply handle the query params in your handler:
package main
import (
"net/http"
"github.com/gorilla/mux"
)
func HealthCheckHandler(w http.ResponseWriter, req *http.Request) {
values := req.URL.Query()
foo := values.Get("foo")
if foo != "" {
w.Write([]byte("Healthy " + foo))
} else {
w.Write([]byte("Healthy <foo>"))
}
w.WriteHeader(http.StatusOK)
}
func main() {
r := mux.NewRouter()
r.HandleFunc("/health-check", HealthCheckHandler)
http.ListenAndServe(":8080", r)
}
according to the gorilla/mux documentation, the Queries method is meant to match your handler to specific functions, akin to a regular expression.

Request created with http.NewRequestWithContext() looses context when passed to middleware

In program bellow I have two routers. One is working at localhost:3000 and acts like a public access point. It also may send requests with data to another local address which is localhost:8000 where data is being processed. Second router is working at localhost:8000 and handles processing requests for the first router.
Problem
The first router sends a request with context to the second using http.NewRequestWithContext() function. The value is being added to the context and the context is added to request. When request arrives to the second router it does not have value that was added previously.
Some things like error handling are not being written to not post a wall of code here.
package main
import (
"bytes"
"context"
"net/http"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
)
func main() {
go func() {
err := http.ListenAndServe(
"localhost:3000",
GetDataAndSolve(),
)
if err != nil {
panic(err)
}
}()
go func() {
err := http.ListenAndServe( // in GetDataAndSolve() we send requests
"localhost:8000", // with data for processing
InternalService(),
)
if err != nil {
panic(err)
}
}()
// interrupt := make(chan os.Signal, 1)
// signal.Notify(interrupt, syscall.SIGTERM, syscall.SIGINT)
// <-interrupt // just a cool way to close the program, uncomment if you need it
}
func GetDataAndSolve() http.Handler {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Get("/tasks/str", func(rw http.ResponseWriter, r *http.Request) {
// receiving data for processing...
taskCtx := context.WithValue(r.Context(), "str", "strVar") // the value is being
postReq, err := http.NewRequestWithContext( // stored to context
taskCtx, // context is being given to request
"POST",
"http://localhost:8000/tasks/solution",
bytes.NewBuffer([]byte("something")),
)
postReq.Header.Set("Content-Type", "application/json") // specifying for endpoint
if err != nil { // what we are sending
return
}
resp, err := http.DefaultClient.Do(postReq) // running actual request
// pls, proceed to Solver()
// do stuff to resp
// also despite arriving to middleware without right context
// here resp contains a request with correct context
})
return r
}
func Solver(next http.Handler) http.Handler { // here we end up after sending postReq
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if r.Context().Value("str").(string) == "" {
return // the request arrive without "str" in its context
}
ctxWithResult := context.WithValue(r.Context(), "result", mockFunc(r.Context()))
next.ServeHTTP(rw, r.Clone(ctxWithResult))
})
}
func InternalService() http.Handler {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.With(Solver).Post("/tasks/solution", emptyHandlerFunc)
return r
}
Your understanding of context is not correct.
Context (simplifying to an extent and in reference to NewRequestWithContext API), is just an in-memory object using which you can control the lifetime of the request (Handling/Triggering cancellations).
However your code is making a HTTP call, which goes over the wire (marshaled) using HTTP protocol. This protocol doesn't understand golang's context or its values.
In your scenario, both /tasks/str and /tasks/solution are being run on the same server. What if they were on different servers, probably different languages and application servers as well, So the context cannot be sent across.
Since the APIs are within the same server, maybe you can avoid making a full blown HTTP call and resort to directly invoking the API/Method. It might turn out to be faster as well.
If you still want to send additional values from context, then you'll have to make use of other attributes like HTTP Headers, Params, Body to send across the required information. This can provide more info on how to serialize data from context over HTTP.

How to have static HTML sites in a flutter web application?

I have a Project in Flutter Web for which I'd like to create a landing page in pure HTML for SEO purposes.
My intention is to create a Server that serves the static files with higher priority and if the static HTML files would return an error on the request, the built flutter web project should be used as fallback.
I created this server in Golang:
package main
import (
"net/http"
)
func main() {
http.ListenAndServe(":2000", professionalServer{})
}
type professionalServer struct{}
var flutterServer = http.FileServer(http.FileSystem(http.Dir("../../../professional/build/web")))
var staticServer = http.FileServer(http.FileSystem(http.Dir("../../../professional/landing-page/dist")))
func (professionalServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
res := preflightResponseWriter{OutputData: make(map[string]int)}
staticServer.ServeHTTP(res, r)
if res.OutputData["status_code"] == 200 {
staticServer.ServeHTTP(w, r)
} else {
flutterServer.ServeHTTP(w, r)
}
}
type preflightResponseWriter struct {
OutputData map[string]int
}
func (preflightResponseWriter) Header() http.Header {
return http.Header{}
}
func (preflightResponseWriter) Write([]byte) (int, error) {
return 0, nil
}
func (p preflightResponseWriter) WriteHeader(statusCode int) {
p.OutputData["status_code"] = statusCode
}
This would actually work, but the problem is that Flutter Web uses the hash format for routes (i.e. http://website.com/#/dashboard) and browser don't send the part that comes after the hashtag, so my golang server sees http://website.com/, then it checks if the static file server can handle this URL, which it can and so the static file server sends the response.
How can I fix this? Is it possible to send the full URL to the server, including the part that comes after #?
Thanks for your help in advance!
How I solved it inspired by the suggestion I got from #ResamVi:
I followed the steps from the answer so that my app finally has the base href /app/.
Then, in order to make the server work properly, I did these changes to my server file:
package main
import (
"net/http"
"strings"
)
func main() {
http.ListenAndServe(":2000", professionalServer{})
}
type professionalServer struct{}
var flutterServer = http.StripPrefix("/app/", http.FileServer(http.FileSystem(http.Dir("../../../professional/build/web"))))
var staticServer = http.FileServer(http.FileSystem(http.Dir("../../../professional/landing-page/dist")))
func (professionalServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/app") {
rw := preflightResponseWriter{OutputData: make(map[string]int)}
flutterServer.ServeHTTP(rw, r)
if rw.OutputData["status_code"] >= 400 && rw.OutputData["status_code"] < 500 {
http.ServeFile(w, r, "../../../professional/build/web/index.html")
} else {
flutterServer.ServeHTTP(w, r)
}
} else {
staticServer.ServeHTTP(w, r)
}
// check if starts with /app
// if no -> staticServer
// if yes:
// simulate request, check if response code is ok
// if response code is ok, serve via flutterServer.Serve
// else serve file directly
}
type preflightResponseWriter struct {
OutputData map[string]int
}
func (preflightResponseWriter) Header() http.Header {
return http.Header{}
}
func (preflightResponseWriter) Write([]byte) (int, error) {
return 0, nil
}
func (p preflightResponseWriter) WriteHeader(statusCode int) {
p.OutputData["status_code"] = statusCode
}
Now, requests that start with /app will either load an asset for the flutter web app or, if there's no asset fulfilling the request, the index.html will be loaded.
If the request URL doesn't start with /app, the static files are served.
This may be more suitable as a comment where I'd ask clarify if you want to change the web app's URL strategy but I don't have the rep for that (forgive me lord, but this rule is a bit restrictive)
If you are free to change the URL strategy you can do so by adding the dependency at the appropriate place as described in the link above:
import 'package:url_strategy/url_strategy.dart';
void main() {
setPathUrlStrategy();
runApp(MyApp());
}
Refer also to: How to remove hash (#) from URL in Flutter web

go lang net/http request pattern

I'm writing a simple rest api server and I cannot get route dynamic url using net/http
http://localhost:8080/book/name
where name can be any string.
this was my attempt:
func viewIndex(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, 'html')
}
http.HandleFunc("/book/{name}", view)
this does not work, there is some cryptic note in the documentation of HandleFunc:
The documentation for ServeMux explains how patterns are matched.
This is a working solution.
The only interesting part it that patterns ending with / will be treated as prefix matcher for all urls.
So pattern /book/ will match:
/book/
/book/a
/book/bb
/book/a/b
sample simplified code:
package main
import (
"fmt"
"strings"
"net/http"
)
func book(name string) {
fmt.Printf("requesting book '%s'\n",name);
}
func view(w http.ResponseWriter, r *http.Request) {
url := r.URL.String()
if strings.HasPrefix(url, "/book/"){
name := url[6:]
book(name)
fmt.Fprint(w, name)
}
}
func main() {
http.HandleFunc("/book/", view)
http.ListenAndServe("localhost:8080", nil)
}

Unable to read variables from a url using gorilla mux in golang

I am trying to write a unit test using gotests and gomock to my restful service written in golang using gorilla but service fails to get variables from the url
Here is my request
req, err := http.NewRequest("GET", "product/5b5758f9931653c36bcaf0a0", nil)
actual endpoint is product/{id}
when I step into my service at the below code
params := mux.Vars(req)
params map is empty when it should have id key mapped to 5b5758f9931653c36bcaf0a0
Strange part is endpoint works fine from post man.
May I know whats wrong with the request?
This solved the issue
req = mux.SetURLVars(req, map[string]string{"id": "5b5758f9931653c36bcaf0a0"})
Since you're using GET requests, you can use the http.Get function, it works as expected:
package main
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
)
func handle(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
fmt.Println(params)
}
func main() {
m := mux.NewRouter()
m.HandleFunc("/products/{id}", handle)
http.Handle("/", m)
go func() {
http.ListenAndServe(":8080", nil)
}()
_, err := http.Get("http://localhost:8080/products/765")
// Handle Error
}
If you really want to use http.NewRequest, that function doesn't actually execute the request so here is what you would need:
req, err := http.NewRequest("GET", "product/5b5758f9931653c36bcaf0a0", nil)
client := &http.Client{}
client.Do(req)
Create the mux Router in a separate function in the source code and call that directly in your test.
In Source code:
func Router() *mux.Router {
r := mux.NewRouter()
r.HandleFunc("/product/{id}", productHandler)
return r
}
func main() {
http.Handle("/", Router())
}
In Test:
func TestProductHandler(t *testing.T) {
r := http.NewRequest("GET", "product/5b5758f9931653c36bcaf0a0", nil)
w := httptest.NewRecorder()
Router().ServeHTTP(w, r)
}
Found the related solution in one of the google groups forums.
https://groups.google.com/forum/#!msg/golang-nuts/Xs-Ho1feGyg/xg5amXHsM_oJ