treq icon indicating copy to clipboard operation
treq copied to clipboard

Implement fail_for_status

Open dmurvihill opened this issue 8 years ago • 20 comments

This commit adds a fail_for_status method to the Response object analogous to the 'raise_for_status' method in Request, to save users some time writing boilerplate status code checking.

dmurvihill avatar Dec 28 '16 22:12 dmurvihill

Codecov Report

Merging #159 into master will decrease coverage by 2.01%. The diff coverage is 100%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #159      +/-   ##
==========================================
- Coverage   98.71%   96.69%   -2.02%     
==========================================
  Files          20       20              
  Lines        1713     1817     +104     
  Branches      136      155      +19     
==========================================
+ Hits         1691     1757      +66     
- Misses         12       39      +27     
- Partials       10       21      +11
Impacted Files Coverage Δ
src/treq/response.py 92.85% <100%> (-3.92%) :arrow_down:
src/treq/__init__.py 100% <100%> (ø) :arrow_up:
src/treq/test/test_response.py 89.74% <100%> (-10.26%) :arrow_down:
src/treq/test/test_testing.py 100% <100%> (ø) :arrow_up:
src/treq/test/util.py 53.12% <0%> (-46.88%) :arrow_down:
src/treq/test/test_multipart.py 92.82% <0%> (-4.89%) :arrow_down:
src/treq/test/test_treq_integration.py 94.04% <0%> (-0.93%) :arrow_down:
src/treq/client.py 97.58% <0%> (-0.39%) :arrow_down:
... and 6 more

Continue to review full report at Codecov.

Legend - Click here to learn more Δ = absolute <relative> (impact), ø = not affected, ? = missing data Powered by Codecov. Last update 22937cd...4b51d3c. Read the comment docs.

codecov-io avatar Dec 28 '16 22:12 codecov-io

Hi @exarkun, I pushed an update. I believe the only question outstanding is whether you want to use the status code constants from Twisted even though integer literals are used in the rest of the test file.

dmurvihill avatar Mar 28 '17 21:03 dmurvihill

It looks like this branch might have gotten mixed up with #157 ?

glyph avatar Mar 29 '17 08:03 glyph

Oh boy... I'll fix that later today.

dmurvihill avatar Mar 29 '17 17:03 dmurvihill

k @glyph @exarkun, this is fixed.

dmurvihill avatar Mar 30 '17 21:03 dmurvihill

I think the implementation here should match the existing ones -- a global function that you can attach as a callback, and then a method that calls it on self if you already have a Response object. The convenient pattern seems like it's been treq.get("http://example.com").addCallback(treq.content), so it'd be nice to be able to inject this into that -- i.e. treq.get("http://example.com").addCallback(treq.fail_for_status).addCallback(treq.content)

Julian avatar Apr 21 '17 18:04 Julian

Thanks @Julian, the method check_status has been made available as a global function.

dmurvihill avatar Apr 23 '17 16:04 dmurvihill

Fixed conflicts and squashed the commits. Is anyone interested in this feature?

dmurvihill avatar Jul 13 '17 21:07 dmurvihill

This is definitely something Treq should have in some capacity.

I can understand if @dmurvihill isn't still around to finish responding to review and clean up conflicts - if not, @twm, do you think you could take this over and drive it to landing?

glyph avatar Jun 17 '19 22:06 glyph

@glyph Sure, though it may be a while (a few weeks) as I'm pretty busy right now.

twm avatar Jun 24 '19 06:06 twm

why return a failure rather than raise an exception?

graingert avatar Dec 19 '19 10:12 graingert

Failure is the asynchronous version of an exception -- the error has to be delivered to whoever made the request, not in the middle of the reactor spinning itself.

(/me still +1 too for this being great to add)

Julian avatar Dec 19 '19 10:12 Julian

@Julian but constructing the failure is synchronous, it's misleading to return a deferred unless you expect to perform asynchronous work

async def amain():
    response = await treq.get(url)
    response.raise_for_status()

defer.ensureDeferred(amain())

graingert avatar Dec 19 '19 12:12 graingert

Failure is the asynchronous version of an exception -- the error has to be delivered to whoever made the request, not in the middle of the reactor spinning itself.

(/me still +1 too for this being great to add)

What makes you think the error would be delivered to anyone other than the caller of raise_for_status?

graingert avatar Dec 19 '19 12:12 graingert

"Fail" is the right term because the API needs to consume the response body. Something like:

@defer.inlineCallbacks
def txmain(reactor):
    response = yield treq.get(url)
    yield response.expect_status()
    body = yield response.content()

task.react(txmain, ())

It's important to consume the body because otherwise the connection won't be returned to the pool (effectively it leaks until the server times it out — 🤞 the server will do that before you run out of FDs).

This is a bit awkward and I think it would be prone to misuse — it's not obvious why you'd need to yield such a function, so folks are likely to forget to do so. For that reason I was considering making it part of the initial call. Something like:

@defer.inlineCallbacks
def txmain(reactor):
    response = yield treq.get(url, expect_status=200)
    body = yield response.content()

This is inspired by Elm's HTTP package.

We might want to have a more general "expectation" concept, which could handle things like timeouts, maximum response size, response Content-Type, etc. Treq seems like a good place for this stuff as it's the "higher level" HTTP library.

twm avatar Dec 19 '19 22:12 twm

But why would fail/raise_for_status consume the body? Avoiding resource leaks is a separate concern:

async def amain():
    async with treq.get(url) as response:
        response.raise_for_status()
        body = await response.content()

task.react(lambda _: defere.ensureDeferred(amain(), ())

graingert avatar Dec 19 '19 23:12 graingert

Ah, I still live in a Python 2.7-compatible world, so async with didn't occur to me. However the other thing I had in mind is including the response body (or size-limited version thereof) in the exception.

twm avatar Dec 20 '19 23:12 twm

You can still avoid resource leaks in py2, using a continuation like _ConcurrencyMixin:

@defer.inlineCallbacks
def txmain():
    @defer.inlineCallbacks
    def continuation(response)
        response.raise_for_status()
        body = yield response.content()

    yield treq.get(url)(continuation)

task.react(lambda _: txmain(), ())

I don't see a need for raise_for_status to consume the body and/or return a Deferred, what if the response is an EventSource and never completes?

graingert avatar Dec 20 '19 23:12 graingert

I don't follow your example. How does it consume the body when raise_for_status() raises an exception?

As for a streaming response, that's what timeouts and response size limits are for.

twm avatar Dec 21 '19 00:12 twm

I don't follow your example. How does it consume the body when raise_for_status() raises an exception?

the continuation here is like the body of a context manager, when the continuation ends treq can close the channel associated with the request: (There's no need to consume the body)

treq.get can return an instance of:

class _ResponseDeferred(defer.Deferred):
    @defer.inlineCallbacks
    def __call__(self, continuation):
        response = yield self
        try:
            defer.returnValue((yield continuation(response)))
        finally:
            yield closeChannel(response)

graingert avatar Dec 21 '19 00:12 graingert

No activity since 2019, it seems that there are still some open design questions, and there are conflicts. Feel free to resurrect and reopen but I'm going to close to remove clutter from the PR list.

glyph avatar May 03 '23 17:05 glyph