JuliaWeb / Mux.jl

Middleware for Julia
Other
277 stars 67 forks source link

Middleware stacking and the response dict #24

Closed essenciary closed 3 years ago

essenciary commented 8 years ago

I might be missing something, but it seems to me that I can push middlewares at the beginning of the stack, before the routes are evaluated; but once a route is matched, the response is sent directly to the browser, skipping middlewares added after the routing.

For practical purposes, the routing middleware should, like any other middleware, have access to and modify the request and response dictionaries, and return them to the next middleware in the stack. An obvious application of this is a response logger. I've been able to add a request logger at the top of the middlewares stack, but the response logger, added at the bottom of the middleware stack is never called.

Any advice on how could this be achieved? (a)

Related to this, can you please point me in the right direction in regards to: (b) how can I get access to the response dict, similar to the request dict (in HttpServer they're both used equally) (c) how can I setup communication between HttpServer and Mux so that for example application errors are logged back to the console / logger, rather than only having them displayed in the web page response (imagine an app running unmanaged in production, devs would never know about errors displayed to users).

MikeInnes commented 8 years ago

So to log requests, responses, and errors, you probably want something like:

function logger(app, req)
  try
    @show req
    response = app(req)
    @show response
  catch e
    @show e
  end
end

The request goes down the stack via the req parameter, and the response goes back up it through the return value of the function. So you have access to the response at your level in the stack just by taking a look at that return value.

Let me know if that wasn't what you were look for, though.

essenciary commented 8 years ago

Thanks for your reply - sorry for my late follow up. Let me try it and I'll get back to you.

essenciary commented 8 years ago

@one-more-minute The logger example is nice, thanks - I was under the impression that a middleware must always return

return app(req)

in order for the request to go up the stack.

Also nice that I can access the response data - but what about the headers and the other properties? Also, how can I alter the response body and headers from within the middleware?

I have in mind something like this:

using Mux

function logger(app, req)
  @show req
  response = app(req) # this response is not the HttpServer Response - it's just a string, it does not have the :headers, :status, :data fields 
  @show response
end

function etag(app, req) # compute hash based on response headers and response body, set an etag response header and push up the stack
  println("in etag") # <-- never gets here
  response = app(req) # get the response at that point, headers and body
  response.data = response.data * "<h3>etag</h3>" # append to body 
  response.headers["ETag"] = "foo" # add some headers
  return app(req) # push up the stack
end

function rendering(app, req)
  # this should finally render the response body, setting all the headers
end

@app test = (
  Mux.defaults,
  stack(logger), 
  page("/", req -> "<h1>Hello, world!</h1>"), # this should not render, but set the response headers and body and send them up the stack
  stack(etag), # <- never gets here
  stack(rendering) # <- never gets here
)

@sync serve(test)
bauglir commented 8 years ago

Technically @one-more-minute's example does 'return' the response from up the stack, as @show returns whatever it 'shows'. For a request to progress up the stack, you must call app(req). You can either directly return the 'response' you get from up the stack or alter it and return that down the stack.

The request goes up the chain of middlewares for as long as the middlewares keep calling the next one in line. Once this doesn't happen, the response is returned down the stack through all middlewares. So the reason your etag and rendering middlewares never get called is because the page middleware creates an 'endpoint' and if it matches the chain stops right there.

Furthermore the middleware that converts your 'response' into a Response from HttpCommon is part of the Mux.defaults stack. So if you want to be able to alter the Response you'll have to unwind that stack and include its components manually in your own stack inserting your middlewares processing the Response where appropriate.

I'm still getting to grips with Mux.jl a bit myself, so some of this information may be a bit off. But what I've described is the way I currently add headers and such. So if there are any errors in my reasoning someone else is welcome to point them out.

essenciary commented 8 years ago

@bauglir "So the reason your etag and rendering middlewares never get called is because the page middleware creates an 'endpoint' and if it matches the chain stops right there." Correct, and that is my issue.

It's probably obvious by now that what I have in mind is a Rack-like architecture (in Ruby). That's a very successful design pattern and is encountered in almost all major programming languages (WSGI in Python, Rack in Ruby, Plug in Elixir, OWIN in .NET, etc).

It's an important part in growing the web ecosystem of the languages as it promotes interoperation and reusability between components (middlewares) and servers.

In this line of thought, the application itself is nothing but a (collection of) middleware(s) (a Rails app is a collection of middlewares), which allows stacking it with other middlewares and even other frameworks (mount a Sinatra app inside a Rails app, for example) and plugging in app servers at will.

In these frameworks, the middlewares communicate using minimal and simple structures. They expose a function/method that takes the request/env as a param and returns an array of 3 elements: the response code, the headers dictionary and the response body.

HttpServer works in a similar way, exposing the request and the response structures, and in my opinion it would be beneficial if Mux would expose them and would make the router just another middleware.

Interesting in regards to the Mux.defaults - I haven't looked in there, I'll check it out, thanks.

MikeInnes commented 8 years ago

Mux is largely designed to follow those frameworks; it's really a superset since it has the usual middleware stacking idea but also happens to allow branching. Other frameworks I've seen only allow completely linear stacks; you can only have a single endpoint and that goes off to your router or whatever (although I should perhaps look into Rack more). I believe you'd face the same issue with them.

Anyway, the way to solve your issue is to move the etag ware so that it has higher priority than the endpoint:

@app test = (
  Mux.defaults,
  logger,
  etag,
  page("/", req -> "<h1>Hello, world!</h1>")
)

If you want you can do req -> Response(...) to generate a Response object that etag will see. Mux also has some internal functions for converting strings/dicts to responses (which are part of the default middleware), so that might be more convenient.

@bauglir It'd be cool if Mux could pull headers from a :headers key in the response dict when generating a Response object. Can help you set up a PR if you're interested in adding that feature, so that you don't have to resort to pulling things apart as much.

essenciary commented 8 years ago

@MikeInnes Yes, you're right. The ETag middleware would have to sit higher in the stack and wait for the router/app to return, similar to the Logger middleware. My suggested workflow was flawed.

In regards to Rack, upon further reading, it seems I remembered it wrong (it's been a while since digging into it). Middlewares that act upon the response, do it in a similar way. See here the simple example code: http://www.integralist.co.uk/posts/rack-middleware.html where the next middleware is called and the response is then stored and modified.

Basically, if I understand correctly, there are 2 ways for the middlewares to handle their work:

  1. do your thing, return and push up the stack - like a request logger for example
  2. call the next middleware and wait for the response to come back. Do stuff with the response. Like your Logger example.

So this should work, I need to give it a try.

I'll dig into Mux.default to see if I can make use of that to manipulate the response headers. Looking at the notfound middleware, it looks like I can use the status() function to set the response code.

Thanks for your help.

MikeInnes commented 8 years ago

No problem – and if there are things like setting headers that are inconvenient right now, I'm happy to work with people to make them easier. Good luck with your work!

essenciary commented 8 years ago

Much appreciated, cheers!

bauglir commented 8 years ago

@MikeInnes I'm still getting to grips a bit with what the nicest API would be for modifying headers and such. If I come up with something nice I'd be happy to send it in as a PR. I'll keep you posted.

essenciary commented 8 years ago

@MikeInnes Back to this one after a long detour into ORM-land. So, a very simple use case: output a JSON response. Hence, the need to set the content type to application/json. From my perspective, the fact that such a trivial task requires digging into the codebase is a clear indication of the fact that the API does not expose all the needed features. The ability for properly outputting JSON responses should be right there with binding it to a port. Finally, it should be noted that understanding Mux is hard. Not necessarily because of Mux itself, but because in order to understand it, one needs to dig deeper and understand HttpServer and WebSockets too.

essenciary commented 8 years ago

So at the moment I'm a bit unsure about the real world utility as a production ready app server. I realize it's not production ready yet, of course - just wondering how hard is it to get it there.

A few other things that need changing to get there, from my perspective:

Are there any plans for continuing the project at a quicker pace? I'm not the kind of person to complain and wait for things to be done. And I'd be happy do them if anybody from the core team could work on this with me: start with a chat session to go over the code and then review and merge pull requests?

Otherwise it does seem simpler to start from scratch with my own implementation of a router on top of HttpServer and WebSockets - which would be a waste of time and energy. I would not be the only one it seems? https://github.com/codeneomatrix/Merly.jl

MikeInnes commented 8 years ago

Hey @essenciary, sorry for the late reply, I'll try to respond to as much as I can here.

Are there any plans for continuing the project at a quicker pace?

Only yours, I think. For my part, Mux does what I need so I'm not going to be spending a lot of time working on it personally. If you're willing to get your hands a little dirty (/ a lot dirty), any contributions you can make to JuliaWeb would be very welcome, and I'll do my best to make sure it's low-friction for you. Within Mux itself I'm happy to help with questions and respond to PRs, and if you're expecting a reply but not getting one then feel free to bump threads and stuff. Other packages will vary but if things aren't moving at all I'm happy to make you a maintainer (same for Mux once you're up and running). And the same goes for anyone else interested in contributing, of course.

So yeah, if you're willing to take a lead on this stuff you can make it go as fast as you want :) Let me know how I can help you get there.

essenciary commented 8 years ago

Hi @MikeInnes, thanks very much for the follow up, you're very kind as always, much appreciated.

Indeed it's obvious that Julia's core team stays true to the primary goal, of being the best tool for number crunching. But definitely, it's great to have low level libraries like HttpServer and WebSockets. And even higher level ones like Escher and Patchwork, though these too are more oriented towards data visualization.

That makes sense, given that Julia is still at v 0.4.x and there's a lot more to do. However, I strongly believe that the best approach to getting Julia to become a mainstream generic programming language (assuming that this is desired in the first place) is a (very) good web framework. That did it for ruby and that did it for elixir (heck, and even for erlang!). Fintech alone, though very attractive financially for developers (see F#), is too niche to make a language mainstream (again, see F# vs C#).

Nuff said, end rant. Felt the need to lobby for web dev a bit, maybe I can plant a seed and it will end up as a higher priority on the roadmap towards v1 :)


404, 5xx - good point, makes sense. The thing is, cause you designed it, you know how to approach things "the Mux way". But this is not at all obvious for an outsider. Now that you mentioned it, it makes total sense, indeed.


I was thinking of a mechanism for concurrently handling requests by forking Mux/HttpServer processes. So that we'd have one Mux/HttpServer process per one julia worker (per one CPU core) with each Mux/HttpServer process bound to a port. So on a 4 cores CPU we'd end up having 4 Mux/HttpServer processes, waiting for connections on 8000, 8001, 8002 and 8003. In front of this, we could have an Nginx reverse proxy / load balancer running on port 80, dispatching requests towards ports 8000 - 8003. Makes sense?


The "communicating back" part was a different issue. I was trying to "inject" some functionality for complex routing (HTTP verbs, controller - actions, view rendering, etc) without changing the Mux source code. I did this by using functions with side effects in the Mux route matching logic. (If this doesn't make sense, you can take a look at this, from lines 7 to 34: https://github.com/essenciary/jinnie/blob/v0.3/lib/Jinnie/src/mux_extensions.jl) I remember it was super difficult to debug this because the code was executed in the server process and the errors were only dumped in the response, in the browser. It would be nice for instance to have a channel where exceptions to be automatically pushed so that they'd be accessible for inspection. HttpServer has an error callback which could probably be used for this, but it does not seem to be exposed in Mux? Or maybe have a "devel" debug option, where exception bubble up into the main process?


I'm very interested in the JuliaWeb project. I've put a lot of time and effort in the last 4 months in building the basis of a julia web framework. It's still far from being production ready, but it's going great methinks. So when it comes to julia and web development, just like the pig and the bacon, I'm now beyond involved, I'm committed.

In regards to Mux, I recently realized that I needed significantly more features in the router. Besides resources and HTTP verbs, there's things like URL and path helpers (to convert controller, actions and params back into URLs) for using in views, manipulation of headers for content types and redirects, extended routing logic based on protocol, host, subdomain, etc. Thus I concluded that I could benefit from tighter integration between router and the rest of the framework and rolled out my own app server component, on top of HttpServer.

This already solved most of my issues in regards to header manipulation, exception handling and debugging, logging and forking HttpServer processes. Which makes sense - you're happy with Mux as a small agile core, while I need more features catering for a full stack framework.


Again, thanks for your help - I hope soon enough I'll be able to bring good news to the julia community in regards to web development! And I'd definitely love to see the framework as a part of JuliaWeb :)

jpsamaroo commented 5 years ago

Do we want to keep this issue open any longer? I think at this point it's clear that Mux probably won't grow much beyond it's simple core + utilities; and that's probably a good thing, since not everyone needs the same features that others desire.

I would personally recommend we close this issue, and then implement some of the ideas mentioned here in separate packages. The web world is broad and full of different ways to do the same things, and I think it would be best to reflect that in how we lay out the "Mux ecosystem". Small core, with extra packages to add functionality on top.