Repository navigation
Hobby Design #2
Replies: 2 comments
Open Question ResolutionsThe initial implementation (PR #4) resolves the five open questions from the design document: 1. Handler Signatureinterface val Handler
fun apply(ctx: Context ref) ?Handlers are 2. Integration ArchitectureThree internal layers, one public type:
Users never implement 3. Post-Processing MechanismJennet-style interface val Middleware
fun before(ctx: Context ref) ?
fun after(ctx: Context ref) => None
4. Concrete Handled-vs-Errored RepresentationContext has 5. Route GroupsDeferred. Not part of this implementation, as the design document states it should only be pursued if we find an API that's genuinely less error-prone than per-route middleware. Additional Design Decisions
|
Uh oh!
There was an error while loading. Please reload this page.
Hobby is an HTTP web framework for Pony, inspired by Jennet and powered by Stallion.
Background
Jennet was a pioneering Pony web framework that established good patterns for route registration, radix tree routing, and middleware composition. Hobby builds on what Jennet established while evolving the design in areas where we see opportunities for improvement — particularly around control flow in middleware chains, type safety for inter-middleware communication, and leveraging Stallion's more capable HTTP server layer.
Stallion is a protocol-level HTTP/1.x server built on lori. Its "your actor IS the connection" architecture eliminates the hidden-actor and factory patterns of earlier Pony HTTP libraries, giving frameworks direct
refaccess inside the connection actor with synchronous callbacks.Design Principles
Separate "handled" from "errored" in the middleware chain
Jennet used Pony's
errormechanism for middleware short-circuiting (e.g., auth middleware rejecting a request with 401). This conflates normal control flow with actual errors, and has the side effect of skipping middlewareafterphases — which is exactly when you most want logging and cleanup to run.Hobby distinguishes two cases:
afterphases of all preceding middleware still run.errorand indicates a genuine failure.The concrete representation (return type, flag, etc.) is an implementation detail to be determined, but the principle is firm: normal control flow decisions must not use the error channel.
Context data uses typed wrappers with domain types
Middleware needs to communicate with handlers — for example, auth middleware tells the handler who the authenticated user is. The underlying storage is
Map[String, Any val], but raw string keys andascasts are not the intended usage pattern.Middleware authors provide:
AuthenticatedUserandNotAuthenticated, notString.matchto extract values from the context, returning a union of the domain types. Wiring errors (missing key, wrong type) surface aserror, clearly distinct from domain outcomes.This gives three clearly distinct outcomes:
AuthenticatedUserreturnedNotAuthenticatedreturnederror(key missing or wrong type in map)This convention is established through documentation, examples, and the built-in middleware Hobby ships with. The
Map[String, Any val]mechanism is pragmatic; the typed wrapper pattern makes it safe in practice.Routing
Radix tree with standard parameter syntax
Hobby uses a radix tree router, following the approach established by httprouter and adopted by Jennet. Route matching is exact — each request matches at most one route.
Two kinds of dynamic segments:
:name): match a single path segment./users/:idmatches/users/42but not/users/42/posts.*name): match everything from that point forward, must be the last segment./files/*pathmatches/files/css/style.css.Route groups (desired)
Expressing shared middleware once for a set of routes rather than attaching it to each route individually reduces the chance of accidentally leaving a route unprotected. For example, all routes under
/apisharing auth middleware.This is a desired feature, not a guaranteed one. The API design is tricky in Pony — it requires either a way to enter/exit a group context or a group that takes a collection of route definitions. We'll pursue this if we find an API that's genuinely less error-prone than per-route middleware, but won't force an awkward API just to have the feature.
Request and Response Handling
Buffered by default, streaming opt-in
For both request bodies and responses, the default is buffered:
Request bodies: Hobby accumulates the full request body from Stallion's incremental
on_body_chunk()callbacks before invoking the handler. Most handlers want the complete body and shouldn't need to think about chunked arrival. Routes that need to handle large uploads or streaming input can opt into receiving body data incrementally.Responses: Most handlers return a complete response (status, headers, body). For cases that need it — server-sent events, large file downloads — an escape hatch provides access to Stallion's chunked transfer encoding with flow-controlled streaming.
Open Questions
The following are implementation-level decisions deferred until design work begins:
val(shareable, stateless across connections), but the exact interface — return type, how it interacts with the handled/errored distinction — needs design work.HTTPServerActordirectly), but the exact boundary between Hobby's internal actor and user-visible types needs design work.aftermethods on the middleware interface, a separate type, or something else.All reactions