Skip to content

Split StaticFileHandler#call into structured components #15678

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

Open
wants to merge 15 commits into
base: master
Choose a base branch
from

Conversation

straight-shoota
Copy link
Member

@straight-shoota straight-shoota commented Apr 17, 2025

StaticFileHandler#call is a complex method with > 100 loc. This patch extracts individual aspects into helper methods which gives it more structure.
This also makes it easy for inheriting types to inject partial custom behaviour by overriding individual helper methods (see kemalcr/kemal#714 for an example).

false
end

private def check_request_path!(context : Server::Context, request_path : String) : Bool
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: I'd expect a bang method to raise an exception, not return a boolean. Maybe a name such as #allowed_request_path? would be more explicit to the intent? Then we'd have nice guards:

return unless allowed_request_method?(context)
return unless allowed_request_path?(context, request_path)
return unless valid_redirect_to_expanded_path?(context, request_path, expanded_path, file_info)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like that because these methods are not just predicates. They directly affect the behaviour (e.g. the HTTP response) in case of an error.
The purpose of the exclamation mark is to indicate that the method handles errors itself and just returns a boolean that indicates whether the caller should continue or stop execution.

Copy link
Contributor

@ysbaddaden ysbaddaden Apr 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not fond of the extracted methods still having some implicit expectations and intermixed behavior: validation + action + implicit out-of-scope control-flow. Maybe split again? Granted, it would be much more verbose but the helpers would do one thing.

Note: it could be interesting to wrap it the context + methods into a struct, and then we could have a nice public API! maybe not, a struct would mean it's hard to customize, and a class would mean an allocation (but maybe not a bad idea).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The combination of validation + action forms a self-contained component. The only tricky thing about it is handling control flow in the caller scope.

If we want more granular components, we can split the bodies of the check_ methods into separate helpers, of course. But I don't see much benefit from that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better if we pronounce the action more in the method name?

  • check_request_method! || return -> respond_on_invalid_request_method && return
  • check_request_path! || return -> respond_on_invalid_request_path && return
  • check_redirect_to_expanded_path! || return -> redirect_to_expanded_path && return

Copy link
Contributor

@ysbaddaden ysbaddaden Apr 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe? I still prefer braindead flow-control, and do one thing helpers, but it's prolly just my taste.

Copy link
Member Author

@straight-shoota straight-shoota Apr 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be clear, you mean something like this:

unless allowed_request_method?(context)
  return redirect_invalid_request_method(context)
end

unless allowed_request_path?(context, request_path)
  return redirect_invalid_request_path(context, request_path)
end

# EDIT: This has been refactored, see following comment.
# if expanded_path_needs_redirect?(context, request_path, expanded_path, file_info)
#  return redirect_to_normalized_path(context, request_path, expanded_path, file_info)
# end

Especially the latter one would be a bit unfortunate because the redirect needs to check the same conditions again as the validation.
But that's similar for the other as well. The action and condition are strongly connected together. For example, both must consider the same request methods as allowed. If separate methods can be overridden individually, they can get out of sync.

Copy link
Member Author

@straight-shoota straight-shoota Apr 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose the path redirect could easily be refactored to this:

if normalized_path = normalize_request_path(context, request_path, expanded_path, file_info)
  return redirect_to context, normalized_path
end

EDIT: Amended in afe80ec

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may just want to configure the accepted HTTP methods or paths, or overload the checks and call previous_def, but you may also just want to render a custom response, independently from the checks.


return call_next(context) unless file_info

context.response.headers["Accept-Ranges"] = "bytes"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: I'm not sure what to do about this. It doesn't make sense to set this header always. Some implementations might not support ranges for serving files (Kemal, for example, currently doesn't), and we don't support ranges on directory index pages either.

So this should go somewhere in the serve_file method, maybe in the branch that doesn't handle ranges, i.e. directly above the call to serve_file_full?

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

Successfully merging this pull request may close these issues.

2 participants