Skip to content

feat(routing): override default responders via on_request()#2446

Merged
vytas7 merged 38 commits intofalconry:masterfrom
gespyrop:allow-on-request
Jan 25, 2026
Merged

feat(routing): override default responders via on_request()#2446
vytas7 merged 38 commits intofalconry:masterfrom
gespyrop:allow-on-request

Conversation

@gespyrop
Copy link
Copy Markdown
Contributor

@gespyrop gespyrop commented Apr 6, 2025

Add an option to CompiledRouterOptions that allows for overriding the default responders by implementing on_request() in the resource class

Closes #2071

Summary of Changes

Added a new router option (allow_on_request) in falcon.routing.compiled.CompiledRouterOptions that allows for providing a default responder by defining on_request() on the resource. This option is disabled by default. If enabled, on_request() is set as the responder for every unimplemented method except for on_options(). If the option is disabled or on_request() is not provided in the resource, the default responder for "405 Method Not Allowed" is used.

Related Issues

Pull Request Checklist

This is just a reminder about the most common mistakes. Please make sure that you tick all appropriate boxes. But please read our contribution guide at least once; it will save you a few review cycles!

If an item doesn't apply to your pull request, check it anyway to make it apparent that there's nothing to do.

  • Applied changes to both WSGI and ASGI code paths and interfaces (where applicable).
  • Added tests for changed code.
  • Prefixed code comments with GitHub nick and an appropriate prefix.
  • Coding style is consistent with the rest of the framework.
  • Updated documentation for changed code.
    • Added docstrings for any new classes, functions, or modules.
    • Updated docstrings for any modifications to existing code.
    • Updated both WSGI and ASGI docs (where applicable).
    • Added references to new classes, functions, or modules to the relevant RST file under docs/.
    • Updated all relevant supporting documentation files under docs/.
    • A copyright notice is included at the top of any new modules (using your own name or the name of your organization).
    • Changed/added classes/methods/functions have appropriate versionadded, versionchanged, or deprecated directives.
  • Changes (and possible deprecations) have towncrier news fragments under docs/_newsfragments/, with the file name format {issue_number}.{fragment_type}.rst. (Run towncrier --draft to ensure it renders correctly.)

If you have any questions to any of the points above, just submit and ask! This checklist is here to help you, not to deter you from contributing!

PR template inspired by the attrs project.

Add an option to CompiledRouterOptions that allows for overriding the default responders by implementing on_request() in the resource class

Closes falconry#2071
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 6, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (1ff571c) to head (d9b14dc).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff            @@
##            master     #2446   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           64        64           
  Lines         7875      7911   +36     
  Branches      1078      1086    +8     
=========================================
+ Hits          7875      7911   +36     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Member

@vytas7 vytas7 left a comment

Choose a reason for hiding this comment

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

Thanks, that is a great start! 💯

I have still haven't explored all the ramifications of the proposed design, but see some early comments inline.

Copy link
Copy Markdown
Member

@vytas7 vytas7 left a comment

Choose a reason for hiding this comment

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

Thanks for working on this so far, this is starting to shape up 💯

I'll try to find another name for the new option, and clean up the tests/docs a bit.

There is one unresolved issue that I just realized we need to either document clearly, or ideally provide workarounds. When decorating a whole class with hooks such as @falcon.before(...), on_request(...) and on_request_suffix(...) wouldn't get decorated. Which is in a sense good, because otherwise it might be seen as a breaking change (it wasn't decorated before).

However, if we make on_request() support enabled by default in 5.0, we will probably want to sync this behaviour too.

So the simplest and cleanest way is to document this as-is for now, and suggest decorating on_request() separately. However, that will cause double decoration if the class-level decorator is also present, and we change the behaviour in 5.0. Is that OK, or should we try to provide some kind of a clever shim here? Thoughts @gespyrop @CaselIT @kgriffs?

@vytas7 vytas7 requested a review from CaselIT May 18, 2025 13:47
@@ -0,0 +1,6 @@
Added the :attr:`~.CompiledRouterOptions.allow_on_request` router option that
allows for providing a default responder by defining `on_request()` on the
resource. This option is disabled by default. If enabled, `on_request()` is
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If -> When.

also below If the option -> When the option

__slots__ = ('converters',)
allow_on_request: bool
"""Allows for providing a default responder by defining `on_request()` on
the resource.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we could provide a simple example, using maybe req.method to check what method was used, something like

class Responder:
  def on_request(self, req: Request, resp: Response) -> None:
    if req.method == "GET":
      ... # handle get
    elif req.method == "POST":
      ... # handle post
    else:
      raise HTTPMethodNotAllowed

We should also mention that method-named function take precedence, so if a resource defines both on_post and on_request, on_request is not called for posts

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Looks good, I will include the example as it is and I will explain how method-named functions override on_request.

@CaselIT
Copy link
Copy Markdown
Member

CaselIT commented May 20, 2025

There is one unresolved issue

I would expect on_request to be wrapped if the option is enabled. but it may not be that easy to do without changing how the hooks work

@gespyrop
Copy link
Copy Markdown
Contributor Author

I agree that wrapping on_request if the option is enabled makes the most sense. However, accessing the option within the hook is a bit tricky since it introduces a dependency on App.router_options which are not available when the responders get decorated.

@CaselIT
Copy link
Copy Markdown
Member

CaselIT commented May 21, 2025

Yes, it would need to be done dynamically when calling add_route. That likely requires some other change to the decorator so that it can ve accessed later on.

@vytas7
Copy link
Copy Markdown
Member

vytas7 commented May 28, 2025

@gespyrop thanks for the recent update!
Do you think you could try implementing some kind of clever hack for class-level hooks?

@gespyrop
Copy link
Copy Markdown
Contributor Author

@gespyrop thanks for the recent update! Do you think you could try implementing some kind of clever hack for class-level hooks?

Yes, I don't a have a clear plan for this yet but I'll try to figure something out.

@gespyrop
Copy link
Copy Markdown
Contributor Author

I came up with a dirty hack. I don't really like it but it works.

In the hooks I keep a reference of the decorated version of the responder under resource.__decorated_{responder_name}__.
Then, when calling add_route, I check if the option is enabled and assign the decorated responder as the responder.

decorated_responder_name = '__decorated_' + responder_name + '__'
decorated_responder = getattr(resource, decorated_responder_name, None)

if decorated_responder:
    setattr(resource, responder_name, decorated_responder)

Thoughts?

@gespyrop
Copy link
Copy Markdown
Contributor Author

We just may need to tweak the names or where it is stored, on the class or on the wrapped method's attribute.

Storing it on the method's attribute seems cleaner. Shall I name the attribute something like __decorated__?

@vytas7
Copy link
Copy Markdown
Member

vytas7 commented Sep 20, 2025

Looking at this again, maybe this mangling of private variables gets a bit too complex and hard to follow for the user (as in power user who reads Falcon's code)?

I remember we were discussing a similar machinery for suffixed responders when we introduced suffixes, but in the end went for the simple "if it looks like a responder, quacks like a responder, decorate it".

In that spirit, could we alternatively just add a module-level constant in falcon.hooks governing whether ^on_request(_\w+)?$ is decorated or not?
This constant could be initialized from an environment variable along the lines of Custom HTTP Methods (when monkey patching the said constant is infeasible due to the import order).

The default value of this constant would change to True in Falcon 5.0.

Also, when we encounter an on_request generic responder, but decorating it on the class level is disabled, we could emit a warning explaining what's going on? (And how this is going to change in 5.0.)

Thoughts @CaselIT @gespyrop?

@gespyrop
Copy link
Copy Markdown
Contributor Author

gespyrop commented Oct 4, 2025

In that spirit, could we alternatively just add a module-level constant in falcon.hooks governing whether ^on_request(_\w+)?$ is decorated or not?
This constant could be initialized from an environment variable along the lines of Custom HTTP Methods (when monkey patching the said constant is infeasible due to the import order).

This would definitely simplify matters but it would require one additional step from the user, that is setting the value of the environment variable. We could also use this environment variable to determine the default value of ~.CompiledRouterOptions.default_to_on_request.

@vytas7
Copy link
Copy Markdown
Member

vytas7 commented Oct 4, 2025

This would definitely simplify matters but it would require one additional step from the user, that is setting the value of the environment variable. We could also use this environment variable to determine the default value of ~.CompiledRouterOptions.default_to_on_request.

The easier way would be to monkeypatch the said constant, but it might not always be easy depending on the import order of hooks etc. So an envvar would be just an escape hatch for these cases.

Edit: We could still have that environment variable, but it shouldn't really be needed in most "normal" cases, i.e. the below shouldn't be an issue:

>>> import falcon
>>> import falcon.hooks
>>> falcon.hooks.decorate_on_request = True
>>> @falcon.after(my_hook)
... class Resource:
...     ...

All of this is very inelegant for the user, I agree 😞.

However, my rationale is that maybe this is a rather uncommon edge case?
I don't have any authoritative data, but from what I have seen in the wild, the use of class-level hooks isn't really widespread. And the intersection of these users and the adopters of the new on_request() default responder would likely be an even smaller proportion.

The user would get a warning when on_request() is skipped when decorating a class, so they could either monkeypatch the setting, or simply apply decorators to individual methods instead. Or just be aware that on_request() wasn't decorated.

What do you think @CaselIT & @kgriffs?

@gespyrop
Copy link
Copy Markdown
Contributor Author

gespyrop commented Oct 8, 2025

Storing it on the method's attribute seems cleaner. Shall I name the attribute something like __decorated__?

In the meantime, shall I push an implementation of this for reference? It is a cleaner and easier-to-understand approach for the people reviewing the PR.
In summary, instead of storing the decorated responder in private class variables, every potentially decorated responder would store its decorated variant under the __decorated__ attribute.

@vytas7
Copy link
Copy Markdown
Member

vytas7 commented Oct 8, 2025

No, I think let's add a module attribute falcon.hooks.decorate_on_request or similar (default to a value parsed from an environment variable), and don't mangle anything more with private attributes.

We just need to document this, add a warning emitted when on_request is skipped when decorating class, and that's it for the first iteration.

And we need to skip on_websocket from being implemented via on_request, same as we did for on_options. And document it, and I think we'd be good to go.

@gespyrop
Copy link
Copy Markdown
Contributor Author

Done. 👌

Copy link
Copy Markdown
Member

@vytas7 vytas7 left a comment

Choose a reason for hiding this comment

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

@gespyrop I tweaked some stuff to my liking, this is hopefully ready to rock now.
Thanks once again for your effort and patience on this issue!

@vytas7 vytas7 merged commit f81911e into falconry:master Jan 25, 2026
33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow to override the default responders via on_request()

3 participants