Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion context/cluster-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ service "cluster" do
}, [body]]
end

Falcon::Server.middleware(application, cache: false)
Falcon::Server.rack_middleware(application, cache: false)
end
end

Expand Down
17 changes: 17 additions & 0 deletions context/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,23 @@ Then run the application with:
$ falcon serve
~~~

#### Rack Applications Defined in Ruby Files

Rack can load a Ruby file directly and infer the application constant from its filename. For example, `Rack::Builder.parse_file("app.rb")` requires the file and uses `::App` as the Rack application.

Falcon reserves `.rb` serve configurations for protocol HTTP middleware. Existing Rack applications can be exposed through `config/serve.rb` using {ruby Protocol::Rack::Adapter}:

~~~ ruby
# config/serve.rb

require "protocol/rack"
require_relative "../app"

run Protocol::Rack::Adapter.new(App)
~~~

Running `falcon serve` loads `config/serve.rb` as protocol HTTP middleware, while the adapter translates requests and responses for the existing Rack application. This replaces the older `falcon serve --config app.rb` convention without requiring changes to `App` itself.

## Running a Local Server

For local application development, you can use the `falcon serve` command. This will start a local server on `https://localhost:9292`. Falcon generates self-signed certificates for `localhost`. This allows you to test your application with HTTPS locally.
Expand Down
2 changes: 1 addition & 1 deletion examples/cluster/falcon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def url
]
end

Falcon::Server.middleware(rack_application, cache: false)
Falcon::Server.rack_middleware(rack_application, cache: false)
end
end

Expand Down
2 changes: 1 addition & 1 deletion examples/server/standalone.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def self.call(env)
Async do
websocket_endpoint = Async::HTTP::Endpoint.parse("http://127.0.0.1:3000")

app = Falcon::Server.middleware(WebSocketApp)
app = Falcon::Server.rack_middleware(WebSocketApp)

server = Falcon::Server.new(app, websocket_endpoint)

Expand Down
2 changes: 1 addition & 1 deletion examples/utilization/falcon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def call(env)

# Define the middleware stack for this server
middleware do
Falcon::Server.middleware(SimpleApp.new, verbose: false, cache: false)
Falcon::Server.rack_middleware(SimpleApp.new, verbose: false, cache: false)
end

# Define the utilization schema for this service
Expand Down
2 changes: 1 addition & 1 deletion guides/cluster-deployment/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ service "cluster" do
}, [body]]
end

Falcon::Server.middleware(application, cache: false)
Falcon::Server.rack_middleware(application, cache: false)
end
end

Expand Down
17 changes: 17 additions & 0 deletions guides/getting-started/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,23 @@ Then run the application with:
$ falcon serve
~~~

#### Rack Applications Defined in Ruby Files

Rack can load a Ruby file directly and infer the application constant from its filename. For example, `Rack::Builder.parse_file("app.rb")` requires the file and uses `::App` as the Rack application.

Falcon reserves `.rb` serve configurations for protocol HTTP middleware. Existing Rack applications can be exposed through `config/serve.rb` using {ruby Protocol::Rack::Adapter}:

~~~ ruby
# config/serve.rb

require "protocol/rack"
require_relative "../app"

run Protocol::Rack::Adapter.new(App)
~~~

Running `falcon serve` loads `config/serve.rb` as protocol HTTP middleware, while the adapter translates requests and responses for the existing Rack application. This replaces the older `falcon serve --config app.rb` convention without requiring changes to `App` itself.

## Running a Local Server

For local application development, you can use the `falcon serve` command. This will start a local server on `https://localhost:9292`. Falcon generates self-signed certificates for `localhost`. This allows you to test your application with HTTPS locally.
Expand Down
8 changes: 4 additions & 4 deletions lib/falcon/command/serve.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
require_relative "../endpoint"
require_relative "../service/server"
require_relative "../environment/server"
require_relative "../environment/rackup"
require_relative "../environment/serve"

require "async/service/configuration"
require "async/container"
Expand All @@ -32,7 +32,7 @@ class Serve < Samovar::Command
option "-h/--hostname <hostname>", "Specify the hostname which would be used for certificates, etc."
option "-t/--timeout <duration>", "Specify the maximum time to wait for non-blocking operations.", type: Float, default: nil

option "-c/--config <path>", "Rackup configuration file to load.", default: "config.ru"
option "-c/--config <path>", "Application configuration file to load."
option "--preload <path>", "Preload the specified path before creating containers."

option "--cache", "Enable the response cache."
Expand Down Expand Up @@ -72,7 +72,7 @@ def name
# @returns [Async::Service::Environment] The configured server environment.
def environment
Async::Service::Environment.new(Falcon::Environment::Server).with(
Falcon::Environment::Rackup,
Falcon::Environment::Serve,
root: Dir.pwd,

verbose: self.parent&.verbose?,
Expand All @@ -81,7 +81,7 @@ def environment
container_options: self.container_options,
endpoint_options: self.endpoint_options,

rackup_path: @options[:config],
configuration_path: @options[:config],
preload: [@options[:preload]].compact,
url: @options[:bind],

Expand Down
2 changes: 1 addition & 1 deletion lib/falcon/environment/rackup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def rack_app
# Build the middleware stack for the rack application.
# @returns [Protocol::HTTP::Middleware] The middleware stack.
def middleware
::Falcon::Server.middleware(rack_app, verbose: verbose, cache: cache)
::Falcon::Server.rack_middleware(rack_app, verbose: verbose, cache: cache)
end
end
end
Expand Down
73 changes: 73 additions & 0 deletions lib/falcon/environment/serve.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2026, by Samuel Williams.

require "protocol/http/middleware/builder"
require "protocol/rack"

require_relative "../server"

module Falcon
module Environment
# Provides application configuration discovery and loading for `falcon serve`.
#
# When {#configuration_path} is `nil`, {#resolved_configuration_path} looks for
# `config/serve.rb` first and falls back to `config.ru`. An explicit
# {#configuration_path} bypasses this discovery order.
#
# The file extension selects the application interface:
#
# - `.rb` files are evaluated by {Protocol::HTTP::Middleware.load} using the
# protocol HTTP middleware builder interface.
# - `.ru` files are parsed as Rack applications and wrapped with
# {Protocol::Rack::Adapter}.
#
# Both application interfaces respond to `call`, so the configuration file
# extension is the explicit contract rather than inspecting the loaded object.
module Serve
# The explicitly specified application configuration path, if any.
# @returns [String | Nil]
def configuration_path
nil
end

# Resolve the application configuration path.
# @returns [String] The absolute application configuration path.
def resolved_configuration_path
if configuration_path
return File.expand_path(configuration_path, root)
end

serve_path = File.expand_path("config/serve.rb", root)
if File.file?(serve_path)
return serve_path
end

rackup_path = File.expand_path("config.ru", root)
if File.file?(rackup_path)
return rackup_path
end

raise ArgumentError, "Could not find config/serve.rb or config.ru in #{root}!"
end

# Load and wrap the configured application.
# @returns [Protocol::HTTP::Middleware] The middleware stack.
def middleware
path = resolved_configuration_path

case File.extname(path)
when ".rb"
application = ::Protocol::HTTP::Middleware.load(path)
return ::Falcon::Server.protocol_middleware(application, verbose: verbose, cache: cache)
when ".ru"
application = ::Protocol::Rack::Adapter.parse_file(path)
return ::Falcon::Server.rack_middleware(application, verbose: verbose, cache: cache)
else
raise ArgumentError, "Unsupported application configuration: #{path}!"
end
end
end
end
end
23 changes: 18 additions & 5 deletions lib/falcon/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,26 @@
module Falcon
# A server listening on a specific endpoint, hosting a specific middleware.
class Server < Async::HTTP::Server
# Wrap a rack application into a middleware suitable the server.
# @deprecated Use {rack_middleware} instead.
def self.middleware(...)
warn("`Falcon::Server.middleware` is deprecated, use `.rack_middleware` instead.", uplevel: 1, category: :deprecated) if $VERBOSE

return self.rack_middleware(...)
end

# Wrap a Rack application with the standard server middleware.
# @parameter rack_app [Proc | Object] A rack application/middleware.
# @parameter verbose [Boolean] Whether to add the {Middleware::Verbose} middleware.
# @parameter cache [Boolean] Whether to add the {Async::HTTP::Cache} middleware.
def self.middleware(rack_app, verbose: false, cache: true)
def self.rack_middleware(rack_app, verbose: false, cache: true)
return self.protocol_middleware(::Protocol::Rack::Adapter.new(rack_app), verbose: verbose, cache: cache)
end

# Wrap a protocol application with the standard server middleware.
# @parameter application [Protocol::HTTP::Middleware] The protocol application/middleware.
# @parameter verbose [Boolean] Whether to add the {Middleware::Verbose} middleware.
# @parameter cache [Boolean] Whether to add the {Async::HTTP::Cache} middleware.
def self.protocol_middleware(application, verbose: false, cache: true)
::Protocol::HTTP::Middleware.build do
if verbose
use Middleware::Verbose
Expand All @@ -32,9 +47,7 @@ def self.middleware(rack_app, verbose: false, cache: true)
end

use ::Protocol::HTTP::ContentEncoding

use ::Protocol::Rack::Adapter
run rack_app
run application
end
end

Expand Down
6 changes: 6 additions & 0 deletions releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

- Update the Envoy cluster example to use dedicated CDS and EDS services from `async-service-supervisor-envoy` v0.5.

### Rack Compatibility

Falcon is shifting its application boundary from a Rack-centric design to {ruby Protocol::HTTP::Middleware}. `falcon serve` now prefers protocol HTTP middleware configured by `config/serve.rb`, while continuing to discover and run Rack `config.ru` applications through {ruby Protocol::Rack::Adapter}.

Use {ruby Falcon::Server.protocol_middleware} for protocol HTTP applications and {ruby Falcon::Server.rack_middleware} for Rack applications. {ruby Falcon::Server.middleware} is deprecated. Explicit `.rb` serve configurations are now interpreted as {ruby Protocol::HTTP::Middleware}; Rack applications defined in Ruby files should use `config.ru` or wrap the application explicitly with {ruby Protocol::Rack::Adapter}.

## v0.56.0

- Add `Falcon::Environment::Cluster` and `Falcon::Service::Cluster` for running workers with independently bound endpoints.
Expand Down
3 changes: 3 additions & 0 deletions test/falcon/command/.serve/config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# frozen_string_literal: true

run Protocol::HTTP::Middleware::Okay
11 changes: 10 additions & 1 deletion test/falcon/command/serve.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
let(:command) do
subject[
"--port", port,
"--config", File.expand_path("config.ru", __dir__), *options
"--config", File.expand_path(configuration, __dir__), *options
]
end

let(:configuration) {"config.ru"}

it "can listen on specified port" do
configuration = command.configuration
controller = configuration.make_controller
Expand Down Expand Up @@ -56,6 +58,13 @@
it_behaves_like ServeCommand
end

with "a protocol application" do
let(:port) {8096}
let(:configuration) {".serve/config.rb"}

it_behaves_like ServeCommand
end

with "threaded container" do
let(:port) {8092}
let(:options) {["--count", 4, "--threaded"]}
Expand Down
90 changes: 90 additions & 0 deletions test/falcon/environment/serve.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2026, by Samuel Williams.

require "falcon/environment/serve"
require "falcon/environment/server"
require "async/service/environment"
require "sus/fixtures/temporary_directory_context"
require "fileutils"

describe Falcon::Environment::Serve do
include Sus::Fixtures::TemporaryDirectoryContext

let(:evaluator) do
Async::Service::Environment.build(
Falcon::Environment::Server,
subject,
root: root,
name: "localhost",
).evaluator
end

it "prefers config/serve.rb" do
FileUtils.mkdir_p(File.join(root, "config"))
File.write(File.join(root, "config.ru"), "run ->(env) {[200, {}, []]}\n")
File.write(File.join(root, "config/serve.rb"), "run Protocol::HTTP::Middleware::Okay\n")

expect(evaluator.resolved_configuration_path).to be == File.join(root, "config/serve.rb")
end

it "loads config/serve.rb as protocol middleware" do
FileUtils.mkdir_p(File.join(root, "config"))
File.write(File.join(root, "config/serve.rb"), "run Protocol::HTTP::Middleware::Okay\n")

expect(evaluator.middleware).to be_a(Protocol::HTTP::Middleware)
end

it "falls back to config.ru" do
File.write(File.join(root, "config.ru"), "run ->(env) {[200, {}, []]}\n")

expect(evaluator.resolved_configuration_path).to be == File.join(root, "config.ru")
end

it "uses an explicit configuration path" do
evaluator = Async::Service::Environment.build(
Falcon::Environment::Server,
subject,
root: root,
name: "localhost",
configuration_path: "application.rb",
).evaluator

expect(evaluator.resolved_configuration_path).to be == File.join(root, "application.rb")
end

it "discovers a configuration when the explicit path is nil" do
File.write(File.join(root, "config.ru"), "run ->(env) {[200, {}, []]}\n")

evaluator = Async::Service::Environment.build(
Falcon::Environment::Server,
subject,
root: root,
name: "localhost",
configuration_path: nil,
).evaluator

expect(evaluator.resolved_configuration_path).to be == File.join(root, "config.ru")
end

it "rejects unsupported configuration extensions" do
evaluator = Async::Service::Environment.build(
Falcon::Environment::Server,
subject,
root: root,
name: "localhost",
configuration_path: "application.txt",
).evaluator

expect do
evaluator.middleware
end.to raise_exception(ArgumentError, message: be(:include?, "Unsupported application configuration"))
end

it "fails when no configuration exists" do
expect do
evaluator.resolved_configuration_path
end.to raise_exception(ArgumentError, message: be(:include?, "Could not find"))
end
end
Loading
Loading