Skip to content
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

Support a request "falling through" to later routes #166

Open
cemerick opened this issue Oct 8, 2021 · 1 comment
Open

Support a request "falling through" to later routes #166

cemerick opened this issue Oct 8, 2021 · 1 comment

Comments

@cemerick
Copy link

cemerick commented Oct 8, 2021

When using other web frameworks, I'm used to being able to just return null/nil from handlers in order to say "this handler cannot handle this request", and so the request "falls through" to a later handler. The most common rationale IMO for doing this is to centralize the serving of static assets (and when necessary, 404's) referred to by N different "modules" in monolithic webapps.

Say you have an app with some scopes, corresponding to logically distinct components:

let main () =
  run
  @@ router
  [ scope "/blog" [] ...
  ; scope "/feeds" [] ...
  ; get "/" ...
  ; get "/**" (Dream.static ~loader:static_loader "")]]
  @@ fun _r -> Dream.respond ~status:`Not_Found "sorry" 

It's not uncommon for pages to refer to static assets within the same prefix, e.g. a blog post at /blog/2021/10/08/foo would typically have any images on that page served from the same directory, so e.g. /blog/2021/10/08/picture.png. However, the static handler in the example above would never see those requests, as they are exclusively handled within the scope provided to the router. Therefore, each scope currently needs its own static asset handler and not-found handler, to accommodate serving those static assets, and issuing 404s as needed for requests within that scope.

(An immediate workaround is to just put all static assets under a dedicated prefix like "/static/**", but (a) that's a silly reason to potentially break extant URLs, and (b) is of little help when integrating content generated by other tools.)

I had expected to find an exception type that Dream.router would catch as a signal to move on to the next handler in the route list…but no such thing exists. My proposal is then that such a thing be added: a custom exception type that any handler can raise to indicate that they can't satisfy the provided request (a "response" distinct from a 404), forcing the router to continue attempting to delegate request handling to the next handler in the chain.

If and until such a thing is available, implementing a somewhat more limited approach is possible using middleware:

exception NoResponse

let handleNoResponse (failsafe : Dream.handler) (routes : Dream.middleware) =
  let h = routes failsafe in
  fun request ->
    try
      h request
    with NoResponse ->
      failsafe request

let static_handler = Dream.static ~loader:static_loader ""

let main () =
  run
  @@ handleNoResponse static_handler
  @@ router
  [ scope "/blog" [] ...
  ; scope "/feeds" [] ...
  ; get "/" ...
  ]]

With this arrangement, any handler in the router can raise NoResponse, and the handleNoResponse middleware will ensure that the "failsafe" handler is invoked (which is also used as the last-resort handler for the router). This is slightly less capable than if the router module itself declared and handled such an exception (the handleNoResponse middleware is sort of a top-level catch-all that can't be composed with anything else, in contrast with regular routes and handlers), but gets the job done for my main use case for this pattern.

@yawaramin
Copy link
Contributor

e.g. a blog post at /blog/2021/10/08/foo would typically have any images on that page served from the same directory, so e.g. /blog/2021/10/08/picture.png....just put all static assets under a dedicated prefix like "/static/**", but (a) that's a silly reason to potentially break extant URLs

Another option might be to permanent redirect? Eg,

(* Get the rendered post *)
let get_blog_post req = ...

(* Get the rendered post or redirect if request is for a static asset *)
let get_blog_resource req =
  let target = Dream.target req in

  match Filename.extension target with
  | "" -> get_blog_post req
  | _ -> Dream.redirect ~status:`Moved_Permanently req ("/static" ^ target)

...

Dream.router [
  Dream.scope "/blog" [] [
    Dream.get "/:year/:month/:day/:resource" get_blog_resource
  ];

  Dream.get "/static/**" @@ Dream.static "www/static";
  (* Eg /static/blog/2021/10/08/picture.png *)
]

And then update the static URLs in the blog posts at your leisure?

(b) is of little help when integrating content generated by other tools.

Could you give an example of that? I don't immediately get your meaning here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants