Chi: cors middleware doesn't work with subgroup

Created on 25 Sep 2017  路  11Comments  路  Source: go-chi/chi

import (
    "net/http"
    "github.com/go-chi/cors"
    "github.com/pressly/chi"
)

func main() {
    r := chi.NewRouter()
    cors := cors.New(cors.Options{
        // AllowedOrigins: []string{"https://foo.com"}, // Use this to allow specific origin hosts
        AllowedOrigins: []string{"*"},
        // AllowOriginFunc:  func(r *http.Request, origin string) bool { return true },
        AllowedMethods:     []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
        AllowedHeaders:     []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
        ExposedHeaders:     []string{"Link"},
        AllowCredentials:   true,
        OptionsPassthrough: true,
        MaxAge:             3599, // Maximum value not ignored by any of major browsers
    })

    r.Group(func(r chi.Router) {
        r.Use(cors.Handler)
        r.Get("/", func(w http.ResponseWriter, r *http.Request) {
            w.Write([]byte("welcome"))
        })

    })

    http.ListenAndServe(":3000", r)
}

using postman to simulate a options request and I got a 405 response code. everything work fine only if I remove the subGroup.

env:golang1.9

Most helpful comment

Yep. I started diving through the code after I posted to see if I could offer a PR fixing it when I realised it would be impossible.

The app i'm working on is kind of like an API gateway kind of thing, with some local responses and some proxied responses from upstream APIs and getting the middleware and some of the proxied responses were not playing well together. I was trying to avoid messing too much with the proxied endpoints and thought grouping might help me get around intercepting/changing CORS headers. I'll figure something out.

In the meantime I might make a docs PR over to the cors package to make the inline middleware issue clear and save future devs some time (if they're doing anything as unwise as me)

All 11 comments

@chenjie4255 You are using github.com/pressly/chi that's the older URL for this project. The project change to github.com/go-chi/chi. I tried your code on the correct repository and works fine.

Thanks for your time and guidance @ustrajunior!

@ustrajunior @VojtechVitek sorry for the inconvenience.

I retry with package github.com/go-chi/chi and still got an 405 method not allowed error :(
here is my curl code:

curl -v -X OPTIONS \
  http://localhost:3000/ \
  -H 'cache-control: no-cache' \
  -F Origin=http://www.google.com

respone:

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> OPTIONS / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
> cache-control: no-cache
> Content-Length: 162
> Expect: 100-continue
> Content-Type: multipart/form-data; boundary=------------------------42627caf0fee76ac
> 
< HTTP/1.1 405 Method Not Allowed
< Date: Sat, 30 Sep 2017 02:55:17 GMT
< Content-Length: 0
< Content-Type: text/plain; charset=utf-8
< Connection: close
< 
* Closing connection 0

@chenjie4255 I believe you are getting 405 because you are trying to request a handler that was not defined.

You have defined an endpoint to a GET method. To archive what you are trying, you must define:

r.Options("/",  func(w http.ResponseWriter, r *http.Request) {})

But you don't need this when using the cors middleware.

If you try to do to an ajax request like:

$.ajax({
  url: "http://localhost:3000/",
  context: document.body
}).done(function() {
  $( this ).addClass( "done" );
});

jquery will do a options request before the actual get request and the cors middleware will respond with the configured response.

@ustrajunior, I am quite sure I have already define my handler within the example code. as you can see:

    r.Group(func(r chi.Router) {
        r.Use(cors.Handler)
        r.Get("/", func(w http.ResponseWriter, r *http.Request) {
            w.Write([]byte("welcome"))
        })
    })

only one different from your code is that I am using the curl command directly to send an OPTIONS request, but this should not make any materially changes.

@chenjie4255 Yes, I see. I'm not quite sure why with curl does not work. I know that doing a preflight on the browser works great. Maybe @VojtechVitek can have a better explanation.

"cors middleware doesn't work with subgroup" - does it work without subgroup for you?

If not, can you please submit issue at https://github.com/go-chi/cors instead?

@ustrajunior unfortunately this issue is reported by my frontend teammate. he told me my API does not work since 405 error.

@VojtechVitek yes,It works good without subgroup as I said. I think I need to debug a request my self If this issue cannot be reproduced by you.

I ran into this same issue today and I thought I was going crazy. I'm glad a google finally turned this up.

I can reproduce consistently with a small test case.

package main

import (
    "net/http"

    "github.com/go-chi/chi"
    "github.com/go-chi/cors"
)

func main() {
    cors := cors.New(cors.Options{
        AllowedOrigins:   []string{"*"},
        AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
        AllowedHeaders:   []string{"Accept", "Content-Type", "Authorization"},
        AllowCredentials: true,
        Debug:            true,
    })

    r := chi.NewRouter()
    // r.Use(cors.Handler)

    r.Group(func(r chi.Router) {
        r.Use(cors.Handler)
        r.Get("/foo", func(w http.ResponseWriter, r *http.Request) {
            w.Write([]byte("foo"))
        })
        r.Post("/bar", func(w http.ResponseWriter, r *http.Request) {
            w.Write([]byte("bar"))
        })
    })

    http.ListenAndServe(":8080", r)
}

If you run that code as is with the following curl

curl -H "Origin: http://example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Authorization" \
  -X OPTIONS --verbose \
  http://localhost:8080/bar

It will give you this result

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> OPTIONS /bar HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: */*
> Origin: http://example.com
> Access-Control-Request-Method: POST
> Access-Control-Request-Headers: Authorization
> 
< HTTP/1.1 405 Method Not Allowed
< Date: Sun, 28 Jan 2018 10:25:18 GMT
< Content-Length: 0
< Content-Type: text/plain; charset=utf-8
< 
* Connection #0 to host localhost left intact

with the following in the logs (with debug set on the cors config)

[cors] 2018/01/28 21:24:23 Handler: Preflight request
[cors] 2018/01/28 21:24:23 Preflight response headers: map[Access-Control-Allow-Origin:[http://example.com] Access-Control-Allow-Methods:[POST] Access-Control-Allow-Headers:[Authorization] Access-Control-Allow-Credentials:[true] Vary:[Origin Access-Control-Request-Method Access-Control-Request-Headers]]

If you move the r.Use(cors.Handler) outside of the group (comment it out and uncomment the one I left there) and run the same curl command you get a different result

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> OPTIONS /bar HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: */*
> Origin: http://example.com
> Access-Control-Request-Method: POST
> Access-Control-Request-Headers: Authorization
> 
< HTTP/1.1 200 OK
< Access-Control-Allow-Credentials: true
< Access-Control-Allow-Headers: Authorization
< Access-Control-Allow-Methods: POST
< Access-Control-Allow-Origin: http://example.com
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< Date: Sun, 28 Jan 2018 10:24:23 GMT
< Content-Length: 0
< Content-Type: text/plain; charset=utf-8
< 
* Connection #0 to host localhost left intact

I'm just speculating at this point, but maybe the group tries to be smart and early 405 methods that don't have handlers registered against them for routes match within the group.

hey @joho - I totally see why this is confusing and annoying. Allow me to explain the semantics..

The underlying data structure of Chi's router is built almost identically to the definitions of the routing handlers with Use(), Get(), Post() etc. It forms into a tree structure that has various paths, each request starts from the root node of the tree and traverses until it finds its terminal leaf node. The algorithm will look ahead for matching paths, but middlewares are able to inject themselves between the branches of the tree which has the power to change the final leaf node. The router is mostly consistent, but there is a nuance with inline middlewares as created with Group(). I'll explain..

Once a router has been create (through the routing definitions), it can just sit in memory and used on a read-only basis as a lookup table of sorts.

When a new HTTP request hits the mux, the mux passes the request to the router, which is actually many http.Handler's in a tree structure. Starting from the root node of the router, the router will first pass the request through the attached middlewares, and then after, will search for the handler that matches the request.URL.Path. In the cors example, the cors middleware will execute before searching for /bar, and in fact, it will respond to the OPTIONS request and complete the response right inside of the middleware, and will not go further. Then another GET request to /bar, cors middleware will see it, oh its not an OPTIONS method, go to next http handler, until it reaches our terminal route handler for GET /bar.

However, middlewares defined in a Group() or using With(), are considered inline-middlewares. Instead of a stack of middlewares on the router, these middlewares are inlined to the specific route, and only that route. So, during execution, the cors.Handler middleware will fail to find the http method OPTIONS for /bar, therefore, it will not get executed. It's a subtle nuance and can be annoying, but typically it only comes into play during finding routes, the execution across the handlers is the same.

request -> middlewares(w,r) -> {find handler matching r.URL.Path} -> !found? 405 : inline-middlewares(w,r) -> routeHandler(w,r)

..so, my question is, why can't you have r.Use(cors.Handler) outside of the group as in your example?

otherwise you could define .Options() for /bar to a noop handler, so it would find the handler, but be handled/served by cors.Handler. Or, you could use Handle() for /bar, which would match any http method. But, theres probably a way to move your cors.Handler to root-level and still meet whatever other functionality you're trying to do.

Yep. I started diving through the code after I posted to see if I could offer a PR fixing it when I realised it would be impossible.

The app i'm working on is kind of like an API gateway kind of thing, with some local responses and some proxied responses from upstream APIs and getting the middleware and some of the proxied responses were not playing well together. I was trying to avoid messing too much with the proxied endpoints and thought grouping might help me get around intercepting/changing CORS headers. I'll figure something out.

In the meantime I might make a docs PR over to the cors package to make the inline middleware issue clear and save future devs some time (if they're doing anything as unwise as me)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rocanion picture rocanion  路  4Comments

kevinconway picture kevinconway  路  8Comments

Bartuz picture Bartuz  路  3Comments

hmgle picture hmgle  路  7Comments

MrXu picture MrXu  路  3Comments