The "default" configuration puma level is initialized with a "binds" of `tcp://0.0.0.0:9292`. Puma is designed to be able to bind to multiple ports.
When a `:port` is sent from Rails along with an empty `user_supplied_options` then the port is treated as a "default". This is merged in with the system defaults, and then later converted into a "binds" via calling `config.port` in the `set_host_port_to_config` method.
The bug comes due to the "level" of the configuration. Since both are being set on the same "level" the `port` call does not over-write the existing binds but instead prepends to the array. We can fix by ensuring that any binds in a given "level" are empty before setting it.
There was a mistake previously where if both host and port were passed in as "default" they would take precedence of any values from the puma config "file".
We can fix this by checking to make sure there were values explicitly passed before setting the config.
To build a "binds" we need a host IP (via Host) and a port. We were running into a problem where a Host was being explicitly set via user but the Port was being defaulted to by Rails. When this happened the Host was used, but Puma would accidentally use it's own default port 9292 instead of Rail's port of 3000.
The fix was to use the "default" port passed in from a framework (if available) when no explicitly set Port is provided.
When options are passed to the Puma rack handler it is unknown if the options were set via a framework as a default or via a user. Puma currently has 3 different sources of configuration, the user via command line, the config files, and defaults.
Rails 5.1+ will record the values actually specified by the user versus the values specified by the frameworks. It passes these values to the Rack handler and now it's up to Puma to do something with that information.
When only framework defaults are passed it will set
```
options[:user_supplied_options] = []
```
When one or more options are specified by the user such as `:Port` then those keys will be in the array. In that example it will look like this
```
options[:user_supplied_options] = [:Port]
```
This change is 100% backwards compatible. If the framework is older and does not pass this information then the `user_supplied_options` will not be set, in that case we assume all values are user supplied.
Internally we accomplish this separation by replacing `LeveledOptions` which was a generic way of specifying options with different priorities with a more explicit `UserFileDefaultOptions` this assumes only 3 levels of options and it will use them in the order supplied (user config wins over file based config wins over defaults).
Now instead of using 1 dsl to set all values, we use 3. A user dsl, a file dsl and a Configuration.new` will return all 3 DSLs to the block. It's up to the person using the block to use the correct dsl corresponding to the source of data they are getting.
The RackHandler for Puma has configuration options that I want to test without booting up a server. By separating out into a new method we can do this easily.
It then allow rails to start puma and pass the TLS/SSL configuration:
~~~
bin/rails server puma -b "ssl://0.0.0.0?key=/path/to/example.key&cert=/path/to/example.crt"
~~~
- Currently it's not possible to override the default options for
Puma::Configuration with user provided options.
- I came across this issue while working on fixing server restart for
Rails.
- Rails can send it's own restart command to Puma and Puma should store
it in it's configuration object. So that Puma::Launcher can use it.
- After this patch it will be possible as user provided options will be
taken into account in Configuration object.
The "default" thread in the handler was interpreted as canonical and took precedence over the `config/puma.rb` file. Fixed by using defaults already present in `configuration.rb` which is used by the Launcher.
We only advertise `Puma.cli_config` when puma is set via the cli. Not sure why but if `cli.rb` hasn't been loaded then we don't need to run that code.
Moving requires to Launcher so it can be called as a standalone file (otherwise we get require errors).