-
Notifications
You must be signed in to change notification settings - Fork 181
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[BREAKING]: Overhaul Handlers framework + rewrite Router to be trie-based and support dynamic use-cases #818
Conversation
Codecov Report
@@ Coverage Diff @@
## master #818 +/- ##
==========================================
+ Coverage 78.10% 78.77% +0.66%
==========================================
Files 36 36
Lines 2430 2502 +72
==========================================
+ Hits 1898 1971 +73
+ Misses 532 531 -1
📣 Codecov can now indicate which changes are the most critical in Pull Requests. Learn more |
In terms of broader vision, I still like the idea of including |
Ok, here's something a little more speculative (and perhaps heretical); what we have have the following construct that would allow a very traditional way of defining an "app" similar to other languages/frameworks: mutable struct App
router::Router
middleware::Any
end
App() = App(Router(), nothing)
ismethod(x) = x in (:get, :post, :put, :delete, :head, :options, :patch)
function Base.getproperty(app::App, x::Symbol)
if x === :use
return function(handler)
app.middleware = handler(app.middleware)
end
elseif ismethod(x)
return function(path, handler)
register!(app.router, uppercase(String(x)), path, handler)
end
else
return getfield(app, x)
end
end This would allow you do so something like: const app = HTTP.App()
app.use(logging_middleware)
app.get("/api/widgets", req -> HTTP.Response(200, getWidgets()))
HTTP.serve(app, host, port) So the idea is that we have an I don't know, it's pretty un-Julian in style, but it would certainly feel very similar for those coming from other languages/frameworks. I guess a more Julia style would be something like: const app = HTTP.App()
HTTP.use!(app, logging_middleware)
HTTP.get(app, "/api/widgets", getWidgetsHandler)
HTTP.serve(app, host, port) or something like that. The |
Awesome work on the new router Jacob! Maybe we could take this breaking release as a chance to split it into a new package? To me, this PR (improved router API) and your interesting questions above (about API choices for the middleware framework) show exactly why a split makes sense: there are different philosophies about the best way to write a web framework, and the core HTTP stdlib/package shouldn't fix that decision. By having frameworks as separate packages, HTTP.jl can converge to a stable, unopinionated HTTP implementation, while other framework packages can continue evolving! My motivations behind making the split would be:
|
I'd also prefer anything that isn't necessary to go outside of HTTP. The mime type sniffing is another example, sometimes you want to use mime detection without using HTTP at all. |
I think it is nice to include something in HTTP.jl still. That doesn't stop anyone from building something else or something better (xref #658 and #658 (comment)). |
@fredrikekre Ok. If your thought is that this might be replaced later, the router types and functions shouldn't be exported by default? Perhaps they could be in sub-module, |
I agree with @fredrikekre that keeping the now-very-simple As I stated above, however, I would like to start a Middlewares.jl package that would be an extra repository for common, useful middlewares using the new interface/framework proposed here. That provides an additional layer of separate for those wishing to build other frameworks/middlewares: they can either write their own Perhaps somewhat arrogantly, I'm always drawn to the approach of finding the most natural/best/flexible interface and trying to make that the ecosystem "standard". The Julia ecosystem has had great success with consistency/cohesiveness in this way and I'd like to think we could achieve the same here with the interfaces; both on client-side and server-side. |
I know I've been very absent here, but I just wanted to say: awesome work @quinnj! The new router seems like a great improvement. The middleware stuff also seems like a nice simplification, it will be interesting to try it out. |
Ok, this is the start of the server-side overhaul that matches #789 which was client-side. The main changes here include:
Handler
type in the Handlers moduleHandler
: a function of the formreq::Request -> resp::Response
(technically, there's also a "stream" handler interface of the formstream::Stream -> Nothing
, but that's the more advanced form)Middleware
: a function of the formhandler::Handler -> handler::Handler
The key (and only current) example of a middleware we have right now is:
which is a middleware that takes a request handler and and returns a stream handler that we wrap request handlers with by default in
HTTP.serve
. If the user passesstream=true
, then we won't wrap, assuming they're passing their own stream handler.HTTP.Router
type to a more traditional/standard approach. Routes can now be registered dynamically. They are parsed into a single trie-based structure. We allow ant-style path patterns in paths, which means you can do things like/*/
to wildcard match any segment (we already supported this previously),/**
double wildcard as the final segment to match any length of path after the**
, and named variable matching like/{id}/
, which will be stored in thereq.context[:params]
in aDict
with the path variable name as key. You can also add a regex to conditionally match a variable, like/{id:[0-9]+}/
to only match numbers. Note this doesn't parse the argument, only does the regex match. TheRouter
also supports 405 responses in addition to 404 (405 for when the path matches, but not the method).Planning to add some inline docs here.