Class: Rubino::API::Server
- Inherits:
-
Object
- Object
- Rubino::API::Server
- Defined in:
- lib/rubino/api/server.rb
Overview
Rack app entry point. Wires the middleware stack + router and runs it under Puma.
Reads RUBINO_API_KEY from the environment when no key is passed explicitly; start! refuses to boot without one so the bearer-auth middleware is never bypassed. The pure Rack app (no Puma) is exposed via .build_app for tests and embedding.
server = Rubino::API::Server.new(port: 4820)
server.start!
Constant Summary collapse
- DEFAULT_PORT =
4820- DEFAULT_HOST =
Loopback by default (#69): the server speaks to a shell tool, so a routable bind is opt-in (–host 0.0.0.0 / RUBINO_API_HOST).
"127.0.0.1"
Class Method Summary collapse
-
.bind_url(host:, port:, tls_cert: nil, tls_key: nil) ⇒ #call, String
Composes the Rack middleware stack around the router.
- .build_app(router:, api_key:, logger: Rubino.logger) ⇒ Object
-
.lowlevel_error_handler ⇒ Proc
A Puma lowlevel_error_handler that mirrors ErrorHandler’s error:{code,message} JSON envelope and never exposes the exception class, message, backtrace, Puma version, or file paths.
Instance Method Summary collapse
-
#initialize(port: DEFAULT_PORT, host: DEFAULT_HOST, api_key: nil, router: nil, logger: nil, tls_cert: nil, tls_key: nil) ⇒ Server
constructor
A new instance of Server.
-
#start! ⇒ Object
Boots Puma and blocks.
-
#tls? ⇒ Boolean
Whether this server will serve over TLS.
Constructor Details
#initialize(port: DEFAULT_PORT, host: DEFAULT_HOST, api_key: nil, router: nil, logger: nil, tls_cert: nil, tls_key: nil) ⇒ Server
Returns a new instance of Server.
33 34 35 36 37 38 39 40 41 42 |
# File 'lib/rubino/api/server.rb', line 33 def initialize(port: DEFAULT_PORT, host: DEFAULT_HOST, api_key: nil, router: nil, logger: nil, tls_cert: nil, tls_key: nil) @port = port @host = host @api_key = api_key || ENV.fetch("RUBINO_API_KEY", nil) @router = router || Router.new @logger = logger || Rubino.logger @tls_cert = tls_cert @tls_key = tls_key end |
Class Method Details
.bind_url(host:, port:, tls_cert: nil, tls_key: nil) ⇒ #call, String
Composes the Rack middleware stack around the router. Order matters: Observability is outermost (sees every status, including 500s from ErrorHandler), then ErrorHandler, then RateLimit (so /v1/health and /v1/metrics also get a per-IP ceiling before Auth waves them through), then JsonParser, then Auth closest to the router so unauthorized requests never reach operations.
Builds the Puma bind URL. When a TLS cert+key are configured it returns an ssl:// bind so Puma terminates TLS with the self-signed cert; the web client pins that cert (see Rubino::API::TLS). Otherwise it returns a plain tcp:// bind (local dev / fake stay HTTP).
102 103 104 105 106 107 |
# File 'lib/rubino/api/server.rb', line 102 def self.bind_url(host:, port:, tls_cert: nil, tls_key: nil) return "tcp://#{host}:#{port}" if tls_cert.nil? || tls_key.nil? query = URI.encode_www_form(cert: tls_cert, key: tls_key) "ssl://#{host}:#{port}?#{query}" end |
.build_app(router:, api_key:, logger: Rubino.logger) ⇒ Object
109 110 111 112 113 114 115 116 117 118 |
# File 'lib/rubino/api/server.rb', line 109 def self.build_app(router:, api_key:, logger: Rubino.logger) Rack::Builder.new do use Middleware::Observability, logger: logger use Middleware::ErrorHandler, logger: logger use Middleware::RateLimit use Middleware::JsonParser use Middleware::Auth, api_key: api_key run router end.to_app end |
.lowlevel_error_handler ⇒ Proc
A Puma lowlevel_error_handler that mirrors ErrorHandler’s error:{code,message} JSON envelope and never exposes the exception class, message, backtrace, Puma version, or file paths.
81 82 83 84 85 86 |
# File 'lib/rubino/api/server.rb', line 81 def self.lowlevel_error_handler lambda do |_error, _env = nil, _status = nil| body = JSON.generate(error: { code: "bad_request", message: "bad request" }) [400, { "content-type" => "application/json" }, [body]] end end |
Instance Method Details
#start! ⇒ Object
Boots Puma and blocks. Fails fast if no API key is configured.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/rubino/api/server.rb', line 52 def start! if @api_key.nil? || @api_key.empty? raise ConfigurationError, "RUBINO_API_KEY must be set to start the API server" end app = self.class.build_app(router: @router, api_key: @api_key, logger: @logger) @logger.info(event: "api.server.starting", host: @host, port: @port, tls: tls?) bind_url = self.class.bind_url(host: @host, port: @port, tls_cert: @tls_cert, tls_key: @tls_key) config = Puma::Configuration.new do |c| c.bind(bind_url) c.app(app) c.quiet # Errors raised below the Rack stack (e.g. Puma's HTTP parser rejecting # an oversized QUERY_STRING) bypass ErrorHandler and would otherwise # render Puma's verbose default page — leaking the Puma version and # gem file paths/line numbers (S5-1). Render the same clean envelope # with no internals instead. c.lowlevel_error_handler(Server.lowlevel_error_handler) end Puma::Launcher.new(config).run end |
#tls? ⇒ Boolean
Returns whether this server will serve over TLS.
45 46 47 |
# File 'lib/rubino/api/server.rb', line 45 def tls? !@tls_cert.nil? && !@tls_key.nil? end |