Skip to content
This repository has been archived by the owner on Feb 24, 2024. It is now read-only.

"Feature/Addition Host domain" #2185 was a breaking change. #2222

Closed
sio4 opened this issue Mar 15, 2022 · 8 comments · Fixed by #2226
Closed

"Feature/Addition Host domain" #2185 was a breaking change. #2222

sio4 opened this issue Mar 15, 2022 · 8 comments · Fixed by #2226
Assignees
Labels
bug Something isn't working s: fixed was fixed or solution offered

Comments

@sio4
Copy link
Member

sio4 commented Mar 15, 2022

Description

Oops! #2185 was a breaking change.

Previously, also currently, App embeds Options and Options has Host. So till v1.18.1, we can access configured Host by app.Host and users could use it e.g. https://gobuffalo.io/en/docs/goth/.

type App struct {
	Options
	<...>
type Options struct {
	Name string `json:"name"`
	Addr string `json:"addr"`
	Host string `json:"host"`

By this change, apps referenced the value by app.Host will fail to get the hostname. (Actually, my project used that.)

The workaround is to change applications by replacing app.Host with app.Options.Host but I don't think this is a good thing since we also allow users to access the other values with the same convention like app.Name.

I will check how the change works and what could be a better fix. (considering both of changing the new function's name or add a new function to get the value (including multi-homed Host) or just let it as is.)

It seems like this is more likely a design issue by the way. Please provide any ideas or opinions.

@sio4 sio4 added bug Something isn't working breaking change This feature / fix introduces breaking changes labels Mar 15, 2022
@sio4 sio4 self-assigned this Mar 15, 2022
@paganotoni
Copy link
Member

paganotoni commented Mar 15, 2022

Thanks for the heads up @sio4. This is interesting. Maybe we just need to set Options.Host value in here?

@sio4 Can you expand a bit more on which functionality was broken with the addition of the Host() function? The only change I see in that PR is this one.

@sio4
Copy link
Member Author

sio4 commented Mar 15, 2022

Sure, what I said as a "breaking change" is:

Previously, App.Host was a way to access the structure field App.Host. Actually, it is App.Options.Host but we can access the value as App.Host as usual in golang. One example of the usage of this convention is when configuring buffalo-goth. The code example on the document https://gobuffalo.io/en/docs/goth/ is:

func init() {
  gothic.Store = App().SessionStore

  goth.UseProviders(
    twitter.New(os.Getenv("TWITTER_KEY"), os.Getenv("TWITTER_SECRET"), fmt.Sprintf("%s%s", App().Host, "/auth/twitter/callback")),
    facebook.New(os.Getenv("FACEBOOK_KEY"), os.Getenv("FACEBOOK_SECRET"), fmt.Sprintf("%s%s", App().Host, "/auth/facebook/callback")),
    linkedin.New(os.Getenv("LINKEDIN_KEY"), os.Getenv("LINKEDIN_SECRET"), fmt.Sprintf("%s%s", App().Host, "/auth/linkedin/callback")),
    github.New(os.Getenv("GITHUB_KEY"), os.Getenv("GITHUB_SECRET"), fmt.Sprintf("%s%s", App().Host, "/auth/github/callback")),
  )
}

For now, App.Host is not a field anymore but is a function so the code above will not work as intended. The compilation should be ok, so if a user deploys their app without change but just upgrade buffalo to v0.18.2 or higher, then the app silently becomes malfunctioning. Actually, this is what I exactly faced today with my old app.

I think there are basically two directions.

  1. Rename the new function as something like App.SubApp() (since it actually returns an app) or App.VirtualHost() (as we usually call it as virtual host since Apache era) and keep the original convention of App.Something for all fields.
  2. Just keep it as is and write an upgrade guide. In this case, we also need to add a single line of code inside the new app to make the new sub-app has a proper value of subapp.Options.Host.

If there are more people using the multi-homing feature than using App.Host as a field accessor, the second option could be better but honestly, I prefer the first option since it more makes sense to me. (both to keep the original convention and the function name is more represent what it does)

If we choose the second option, I think we need to keep this in mind and it could be better to choose the first option later when we design and implement v1.

@paganotoni
Copy link
Member

paganotoni commented Mar 15, 2022

Thanks for explaining it @sio4. I think we should take the first route. The purpose of that feature is to limit the app to a single domain (only for it), the first set of names that come to my mind are Only, OnlyForHost, and ForHost.

@paganotoni
Copy link
Member

cc @dmuriel

@sio4
Copy link
Member Author

sio4 commented Mar 16, 2022

Yeah, then I will take a look at the related things and will file a PR. For the naming part, I think we can postpone the decision. For now, I think if the return of the function is the type of App, the name should indicate that. However, as you said, what the function does (or the purpose of the function) is not duplicating an App but just creating a special route rule so it is tricky. I found that App.Group() also returns App and the implementation is very similar. I will check them all and will update the result of the research.

Currently, my ideal idea is that if the function could return a different type of object which can be used as type.GET(...) as the same as App, it could be the best. Roughly, maybe something like that below:

App {
	Options
	Router
}

Router {
	router        *mux.Router
	routes        RouteList
}

func (a *App) Router(h string) * Router {}

instead

App {
	Options
	router        *mux.Router
	routes        RouteList
}

func (a *App) Host(h string) *App {}

During some initial investigation, I found that buffalo routes also broken:

$ buffalo routes
METHOD | PATH | ALIASES | NAME     | HANDLER
------ | ---- | ------- | ----     | -------
GET    | /    |         | rootPath | multi/actions.HomeHandler
GET    | /    |         | rootPath | multi/actions.App.func2
GET    | /    |         | rootPath | multi/actions.App.func3
GET    | /    |         | rootPath | multi/actions.HomeHandler

This issue was caused by RouteInfo.Name() since the current RouteInfo has no information about multi-homed Host value and it always overwrite the first entry of the same Path and Method. So roughly I can get the following output by adding Host information to RouteInfo and modifying related things:

METHOD | HOST                  | PATH | ALIASES | NAME     | HANDLER
------ | ----                  | ---- | ------- | ----     | -------
GET    | dl.example.com        | /    |         | rootPath | multi/actions.App.func1
GET    | api.example.com       | /    |         | rootPath | multi/actions.App.func2
GET    | admin.example.com     | /    |         | rootPath | multi/actions.App.func3
GET    | http://127.0.0.1:3000 | /    |         | rootPath | multi/actions.HomeHandler

Also, it seems like multi-homing is now working if I do app.Host() after I added any route. I did not check this part yet.

Luckily, the document page is not updated yet (gobuffalo/docs#605) so maybe not many users using it :-)

@paganotoni
Copy link
Member

paganotoni commented Mar 17, 2022

I wonder if we could use the Options pattern here. And allow groups and top level applications to be specified a host. The API would be something like:

type ApplicationOption func(*App)
func (a *App) WithOptions(...Options)

// WithHost does both, setting the app.Host and the 
// changes to the mux instance inside the App.
func WithHost(h string) AppOption {
  return func(app *App) {
    app.Host = h
    // changes to the inner router.
  }
}

// Then the usage would be
app.WithOptions(buffalo.WithHost("my.host.com"))

I would love to see Buffalo evolve to use the options pattern instead of maintaining the Options struct. However changing application initialization is out of the scope of this conversation.

@sio4 sio4 removed the breaking change This feature / fix introduces breaking changes label Mar 25, 2022
@sio4
Copy link
Member Author

sio4 commented Mar 25, 2022

Preparation

Below is the test application setup with two Virtual Hosts. The main points are:

  • One Virtual Host is configured before the root App's routes, another is after the root
  • The first Virtual Host has only one middleware while the other gets all of root's
  • The second Virtual Host has paths /, /home as the same as the root App
  • The second Virtual Host has another path /ping
  • Two Resources are configured in a cascaded way
  • Two Groups are configured in the same way
  • Middleware skipping is applied on some routes
		adm := app.VirtualHost("admin.example.com:3000")
		adm.Middleware.Clear()
		adm.Use(paramlogger.ParameterLogger)
		adm.GET("/", func(c buffalo.Context) error {
			return c.Render(http.StatusOK, r.String("Welcome to Admin!"))
		})

		app.GET("/", HomeHandler)
		app.GET("/home", HomeHandler)

		ur := userResource{}
		k := app.Resource("/kings", &ur)
		o := k.Resource("/kongs", &ur)
		o.Middleware.Skip(paramlogger.ParameterLogger, ur.Create, ur.New)
		o.Middleware.Skip(popmw.Transaction(models.DB), ur.New)

		g := app.Group("/group")
		g.GET("/", func(c buffalo.Context) error {
			return c.Render(http.StatusOK, r.String("Hey Group!"))
		})
		g.GET("/in", func(c buffalo.Context) error {
			return c.Render(http.StatusOK, r.String("Hey Group In!"))
		})
		gg := g.Group("/deep")
		gg.GET("/", func(c buffalo.Context) error {
			return c.Render(http.StatusOK, r.String("Hey Deep under the Group!"))
		})

		api := app.VirtualHost("api.example.com:3000")
		api.GET("/", func(c buffalo.Context) error {
			return c.Render(http.StatusOK, r.String("Welcome to API!"))
		})
		api.GET("/home", func(c buffalo.Context) error {
			return c.Render(http.StatusOK, r.String("Welcome to API Home!"))
		})
		api.GET("/ping", func(c buffalo.Context) error {
			return c.Render(http.StatusOK, r.String("Welcome to API Ping!"))
		})

The result of the current version

buffalo routes

METHOD | PATH                                   | ALIASES | NAME                         | HANDLER
------ | ----                                   | ------- | ----                         | -------
GET    | /                                      |         | rootPath                     | multi/actions.App.func5
GET    | /                                      |         | rootPath                     | multi/actions.HomeHandler
GET    | /                                      |         | rootPath                     | multi/actions.App.func5
GET    | /group/                                |         | groupPath                    | multi/actions.App.func2
GET    | /group/deep/                           |         | groupDeepPath                | multi/actions.App.func4
GET    | /group/in/                             |         | groupInPath                  | multi/actions.App.func3
GET    | /home/                                 |         | homePath                     | multi/actions.App.func6
GET    | /home/                                 |         | homePath                     | multi/actions.App.func6
GET    | /kings/                                |         | kingsPath                    | multi/actions.userResource.List
POST   | /kings/                                |         | kingsPath                    | multi/actions.userResource.Create
GET    | /kings/new/                            |         | newKingsPath                 | multi/actions.userResource.New
GET    | /kings/{user_id}/                      |         | kingUserIDPath               | multi/actions.userResource.Show
DELETE | /kings/{user_id}/                      |         | kingUserIDPath               | multi/actions.userResource.Destroy
PUT    | /kings/{user_id}/                      |         | kingUserIDPath               | multi/actions.userResource.Update
GET    | /kings/{user_id}/edit/                 |         | editKingUserIDPath           | multi/actions.userResource.Edit
POST   | /kings/{user_id}/kongs/                |         | kingUserIDKongsPath          | multi/actions.userResource.Create
GET    | /kings/{user_id}/kongs/                |         | kingUserIDKongsPath          | multi/actions.userResource.List
GET    | /kings/{user_id}/kongs/new/            |         | newKingUserIDKongsPath       | multi/actions.userResource.New
GET    | /kings/{user_id}/kongs/{user_id}/      |         | kingUserIDKongUserIDPath     | multi/actions.userResource.Show
PUT    | /kings/{user_id}/kongs/{user_id}/      |         | kingUserIDKongUserIDPath     | multi/actions.userResource.Update
DELETE | /kings/{user_id}/kongs/{user_id}/      |         | kingUserIDKongUserIDPath     | multi/actions.userResource.Destroy
GET    | /kings/{user_id}/kongs/{user_id}/edit/ |         | editKingUserIDKongUserIDPath | multi/actions.userResource.Edit
GET    | /ping/                                 |         | pingPath                     | multi/actions.App.func7
  • There is no multi-homed host information
  • Some lines have a wrong handler name (line 1 and 3, 7 and 8 has the same name)
  • Some has reverse order (/kings/'s GET and POST, /kongs/'s POST and GET)

buffalo task middleware

-> /
	github.com/gobuffalo/buffalo.*App.defaultErrorMiddleware
	github.com/gobuffalo/buffalo.*App.PanicHandler
	github.com/gobuffalo/buffalo.RequestLoggerFunc
	github.com/gobuffalo/mw-forcessl.Middleware.func1
	github.com/gobuffalo/mw-paramlogger.ParameterLogger
	github.com/gobuffalo/mw-contenttype.Set.func1
	github.com/gobuffalo/buffalo-pop/v3/pop/popmw.Transaction.func2
-> /group (see: /)
-> /group/deep (see: /group)
-> /kings (see: /)
-> /kings/{user_id}/kongs (see: /kings)
-> POST /kings/{user_id}/kongs/ (by actions.userResource.Create)
	github.com/gobuffalo/buffalo.*App.defaultErrorMiddleware
	github.com/gobuffalo/buffalo.*App.PanicHandler
	github.com/gobuffalo/buffalo.RequestLoggerFunc
	github.com/gobuffalo/mw-forcessl.Middleware.func1
	github.com/gobuffalo/mw-contenttype.Set.func1
	github.com/gobuffalo/buffalo-pop/v3/pop/popmw.Transaction.func2
-> GET /kings/{user_id}/kongs/new/ (by actions.userResource.New)
	github.com/gobuffalo/buffalo.*App.defaultErrorMiddleware
	github.com/gobuffalo/buffalo.*App.PanicHandler
	github.com/gobuffalo/buffalo.RequestLoggerFunc
	github.com/gobuffalo/mw-forcessl.Middleware.func1
	github.com/gobuffalo/mw-contenttype.Set.func1
  • buffalo task middleware does not print host-specific middlewares

@sio4
Copy link
Member Author

sio4 commented Mar 25, 2022

After the fix

METHOD | HOST                   | PATH                                   | ALIASES | NAME                         | HANDLER
------ | ----                   | ----                                   | ------- | ----                         | -------
GET    | admin.example.com:3000 | /                                      |         | rootPath                     | multi/actions.App.func1
GET    | http://127.0.0.1:3000  | /                                      |         | rootPath                     | multi/actions.HomeHandler
GET    | http://127.0.0.1:3000  | /home/                                 |         | homePath                     | multi/actions.HomeHandler
GET    | http://127.0.0.1:3000  | /kings/                                |         | kingsPath                    | multi/actions.userResource.List
POST   | http://127.0.0.1:3000  | /kings/                                |         | kingsPath                    | multi/actions.userResource.Create
GET    | http://127.0.0.1:3000  | /kings/new/                            |         | newKingsPath                 | multi/actions.userResource.New
GET    | http://127.0.0.1:3000  | /kings/{user_id}/                      |         | kingUserIDPath               | multi/actions.userResource.Show
PUT    | http://127.0.0.1:3000  | /kings/{user_id}/                      |         | kingUserIDPath               | multi/actions.userResource.Update
DELETE | http://127.0.0.1:3000  | /kings/{user_id}/                      |         | kingUserIDPath               | multi/actions.userResource.Destroy
GET    | http://127.0.0.1:3000  | /kings/{user_id}/edit/                 |         | editKingUserIDPath           | multi/actions.userResource.Edit
GET    | http://127.0.0.1:3000  | /kings/{user_id}/kongs/                |         | kingUserIDKongsPath          | multi/actions.userResource.List
POST   | http://127.0.0.1:3000  | /kings/{user_id}/kongs/                |         | kingUserIDKongsPath          | multi/actions.userResource.Create
GET    | http://127.0.0.1:3000  | /kings/{user_id}/kongs/new/            |         | newKingUserIDKongsPath       | multi/actions.userResource.New
GET    | http://127.0.0.1:3000  | /kings/{user_id}/kongs/{user_id}/      |         | kingUserIDKongUserIDPath     | multi/actions.userResource.Show
PUT    | http://127.0.0.1:3000  | /kings/{user_id}/kongs/{user_id}/      |         | kingUserIDKongUserIDPath     | multi/actions.userResource.Update
DELETE | http://127.0.0.1:3000  | /kings/{user_id}/kongs/{user_id}/      |         | kingUserIDKongUserIDPath     | multi/actions.userResource.Destroy
GET    | http://127.0.0.1:3000  | /kings/{user_id}/kongs/{user_id}/edit/ |         | editKingUserIDKongUserIDPath | multi/actions.userResource.Edit
GET    | http://127.0.0.1:3000  | /group/                                |         | groupPath                    | multi/actions.App.func2
GET    | http://127.0.0.1:3000  | /group/in/                             |         | groupInPath                  | multi/actions.App.func3
GET    | http://127.0.0.1:3000  | /group/deep/                           |         | groupDeepPath                | multi/actions.App.func4
GET    | api.example.com:3000   | /                                      |         | rootPath                     | multi/actions.App.func5
GET    | api.example.com:3000   | /home/                                 |         | homePath                     | multi/actions.App.func6
GET    | api.example.com:3000   | /ping/                                 |         | pingPath                     | multi/actions.App.func7
  • All methods are correctly associated
  • It includes Virtual Host information so users can easily distinguish them
  • The printed routes' order is the same as they were registered. It makes users easy to evaluate the real routing decision
  • All resources has the same order of List, Create, New, Show, Update, Destroy, and Edit.

Routing decisions:

  • admin.example.com:3000/ will work fine since it has higher priority than others
  • api.example.com:3000/ will not be used since <default>/ was registered before it
  • However, api.example.com:3000/ping will work since there is no matching route before it
  • /kings/new/ will be evaluated before /kings/{user_id}/ so new cannot be a user_id
-> admin.example.com:3000/
	github.com/gobuffalo/mw-paramlogger.ParameterLogger
-> http://127.0.0.1:3000/
	github.com/gobuffalo/buffalo.*App.defaultErrorMiddleware
	github.com/gobuffalo/buffalo.*App.PanicHandler
	github.com/gobuffalo/buffalo.RequestLoggerFunc
	github.com/gobuffalo/mw-forcessl.Middleware.func1
	github.com/gobuffalo/mw-paramlogger.ParameterLogger
	github.com/gobuffalo/mw-contenttype.Set.func1
	github.com/gobuffalo/buffalo-pop/v3/pop/popmw.Transaction.func2
-> http://127.0.0.1:3000/kings (see: http://127.0.0.1:3000/)
-> http://127.0.0.1:3000/kings/{user_id}/kongs (see: http://127.0.0.1:3000/kings)
-> POST http://127.0.0.1:3000/kings/{user_id}/kongs/ (by actions.userResource.Create)
	github.com/gobuffalo/buffalo.*App.defaultErrorMiddleware
	github.com/gobuffalo/buffalo.*App.PanicHandler
	github.com/gobuffalo/buffalo.RequestLoggerFunc
	github.com/gobuffalo/mw-forcessl.Middleware.func1
	github.com/gobuffalo/mw-contenttype.Set.func1
	github.com/gobuffalo/buffalo-pop/v3/pop/popmw.Transaction.func2
-> GET http://127.0.0.1:3000/kings/{user_id}/kongs/new/ (by actions.userResource.New)
	github.com/gobuffalo/buffalo.*App.defaultErrorMiddleware
	github.com/gobuffalo/buffalo.*App.PanicHandler
	github.com/gobuffalo/buffalo.RequestLoggerFunc
	github.com/gobuffalo/mw-forcessl.Middleware.func1
	github.com/gobuffalo/mw-contenttype.Set.func1
-> http://127.0.0.1:3000/group (see: http://127.0.0.1:3000/)
-> http://127.0.0.1:3000/group/deep (see: http://127.0.0.1:3000/group)
-> api.example.com:3000/
	github.com/gobuffalo/buffalo.*App.defaultErrorMiddleware
	github.com/gobuffalo/buffalo.*App.PanicHandler
	github.com/gobuffalo/buffalo.RequestLoggerFunc
	github.com/gobuffalo/mw-forcessl.Middleware.func1
	github.com/gobuffalo/mw-paramlogger.ParameterLogger
	github.com/gobuffalo/mw-contenttype.Set.func1
	github.com/gobuffalo/buffalo-pop/v3/pop/popmw.Transaction.func2
  • Now it shows all Apps, Groups, Resources, and Virtual Hosts.

@sio4 sio4 linked a pull request Mar 25, 2022 that will close this issue
paganotoni added a commit that referenced this issue Apr 2, 2022
Fixed multi-homing feature and related bugs. (#2222)
@sio4 sio4 closed this as completed Apr 14, 2022
@sio4 sio4 added the s: fixed was fixed or solution offered label Sep 26, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
bug Something isn't working s: fixed was fixed or solution offered
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants