Setting response timeout on a specific view?

Have a (hopefully) quick question that I couldn’t find in the docs anywhere…

Is it possible to change the REQUEST_TIMEOUT configuration value for a specific view? I have one view that I expect to take a variable amount of time depending on the request. But the rest of my views are “fast”.

And as a second question, can you catch the request timeout exception in the view and do some clean up before returning?

Thanks for your help! Heard about Sanic on the TalkPython podcast and have enjoyed playing around with it over the weekend.

@nathanglover I don’t think that would be possible right now, since the REQUEST_TIMEOUT logic is not actually bound to any request instance itself but rather to all of them inside HttpProtocol. You can see about this here.

OTOH, I do think that this can actually be a nice feature to have: it’ll require some effort and rework but the Request lifecycle is awkward and broadly discussed within the community anyway - I even started a thread with everything related in here.

And as a second question, can you catch the request timeout exception in the view and do some clean up before returning?

I don’t know if you can do that in the view because the exception handlers are bound to the application … I guess you can write one specific handler for RequestTimeout or even provide your own ErrorHandler implementation to the application constructor - see here if you want to go deeper in that.

I hope this helps :wink:

1 Like

Thank you @vltr! That helped a lot. For the time being I have just increased the overall RequestTimeout.

1 Like

Great! I’m glad I could help. I’ll leave this open for now, and perhaps move to ideas and discussions so we can debate with other members if this is a nice feature to have. In my opinion it is and is perfectly doable, but, of course, it’ll all depends on the amount of work necessary … But there a lot of things to be done as well. Let’s see :wink:

@core-devs, I moved this thread from help to feature requests since I really found like a nice feature to have - not in the next month of course, but in the future. Let me know your thoughts!

hmmm intersting idea, this is actually harder than it seems. For one thing the timeout right now is measuring the time a connection takes from start to finish vs what your asking for really is limit the amount of time it takes a funciton takes to finish. These are different things and I don’t think we can alter the protocol’s timeout into handling this. You can however build a decorator that wraps a function and raises if it takes too long

# sorry about the russian nesting doll
def timeout(response_timeout):
    def decorator(func):
        async def wrapper(request, *args, **kwargs):
            if inspect.iscoroutinefunction(func):
                coro = func(request, *args, **kwargs)
            else:
                async def awrapper():
                    return func(request, *args, **kwargs)
                coro = awrapper()
            t = asyncio.ensure_future(coro, loop=request.app.loop)
            def timeout_callback():
                t.cancel()
            request.app.loop.call_later(response_timeout, timeout_callback)

            try:
                return await coro
            except asyncio.CancelledError:
                raise RuntimeError('timed out')
        return wrapper
    return decorator

app = Sanic(__name__)

@app.route("/slow")
@timeout(1)
async def test_err(request):
    await asyncio.sleep(3)
    raise RuntimeError('this should be unreachable')

but this still isn’t perfect, it will only trigger if the response handler stuck on some async work, if its doing synchronous work the whole time then it won’t fire. I think you’d need to get signal.alarm involed in some way to make this work with synchronous work but I’m not entirely sure how you’d manage concurrent requests with timeouts.

I started writing something that would accomplish this in the similar way mentioned with exception handlers. It was starting to get much more complicated than originally intended.

I agree that there is a great need for this. One thing that me and @vltr (mostly him) worked hard on with sanic-jwt was allowing lots of levels for configuration modification. In particular, changing config at the route level. For a variety of reasons, this would be hard to implement right now in Sanic, and a lot of settings might not make sense here. But REQUEST_TIMEOUT definitely does make sense, as might REQUEST_MAX_SIZE.

I will add my vote to this as I think it worthwhile to explore further.

1 Like

I know this is an old thread but as a lot of changes has gone through so I was wondering if there was a way now to have a RESPONSE_TIMEOUT by view / handler.

No, there has been no change on this. It is still something in the back of my mind that I would like to add sometime this year.

This came up again in discussion on Discord. Posting a potential solution:

from asyncio import TimeoutError, sleep, wait_for
from functools import wraps
from inspect import isawaitable

from sanic import HTTPResponse, Request, Sanic, json
from sanic.exceptions import ServiceUnavailable

app = Sanic("TestApp")
app.config.RESPONSE_TIMEOUT = 120


def reduced(timeout: int):
    def decorator(f):
        @wraps(f)
        async def decorated_function(request, *args, **kwargs):

            response = f(request, *args, **kwargs)
            if isawaitable(response):
                try:
                    response = await wait_for(response, timeout)
                except TimeoutError:
                    raise ServiceUnavailable("Response timeout")

            return response

        return decorated_function

    return decorator


async def do_response(pause: int) -> HTTPResponse:
    print("Sleeping")
    for _ in range(pause):
        print("> .")
        await sleep(1)
    return json({"pause": pause})


@app.get("/regular/<pause:int>")
async def regular_handler(request: Request, pause: int):
    return await do_response(pause)


@app.get("/reduced/<pause:int>")
@reduced(5)
async def reduced_handler(request: Request, pause: int):
    return await do_response(pause)

Another option is running two apps:

app_regular = Sanic("Regular")
app_extended = Sanic("Extended")
app_extended.config.RESPONSE_TIMEOUT = 120

if __name__ == "__main__":
    app_regular.prepare(port=8001)
    app_extended.prepare(port=8002)
    Sanic.serve()