Skip to content

Abstract the public-facing API away from ClientResponse - #13152

Open
Moist-Cat wants to merge 13 commits into
aio-libs:masterfrom
Moist-Cat:response-refactor
Open

Abstract the public-facing API away from ClientResponse#13152
Moist-Cat wants to merge 13 commits into
aio-libs:masterfrom
Moist-Cat:response-refactor

Conversation

@Moist-Cat

@Moist-Cat Moist-Cat commented Jul 16, 2026

Copy link
Copy Markdown

It's important to notice that the __init__ method varies between implementations and there are some extra fields (e.g, the _in_context field) that are required by some methods used outside the class.
The base class is basically an interface that implements the public API. Fields can't be stored in the base class because they may or may not be available during __init__ so I'm thinking of removing the constructor altogether.

Related issues

#11743

Related PR

#13039

Moist-Cat and others added 2 commits July 15, 2026 23:39
    It's important to notice that the __init__ method varies between
    implementations and there are some leftovers (e.g, the _in_context field).
    The base class is basically an interface.
Comment thread aiohttp/http_base.py Fixed
Comment thread aiohttp/http_base.py Fixed
Comment thread aiohttp/http_base.py Fixed
Comment thread aiohttp/http_base.py Fixed
Comment thread aiohttp/http_base.py Fixed
@Moist-Cat Moist-Cat changed the title Abstract user-facing public API for ClientResponse Abstract the public-facing API away from ClientResponse Jul 16, 2026
@codspeed-hq

codspeed-hq Bot commented Jul 16, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 84 untouched benchmarks
⏩ 83 skipped benchmarks1


Comparing Moist-Cat:response-refactor (6511797) with master (d5d068c)2

Open in CodSpeed

Footnotes

  1. 83 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on master (c0ef574) during the generation of this report, so d5d068c was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@Moist-Cat
Moist-Cat marked this pull request as ready for review July 16, 2026 04:46
@Moist-Cat
Moist-Cat requested a review from asvetlov as a code owner July 16, 2026 04:46
@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.95%. Comparing base (d5d068c) to head (6511797).
⚠️ Report is 3 commits behind head on master.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #13152      +/-   ##
==========================================
- Coverage   98.98%   98.95%   -0.04%     
==========================================
  Files         132      133       +1     
  Lines       49073    49113      +40     
  Branches     2553     2552       -1     
==========================================
+ Hits        48576    48598      +22     
- Misses        373      391      +18     
  Partials      124      124              
Flag Coverage Δ
Autobahn 22.18% <57.01%> (+0.05%) ⬆️
CI-GHA 98.86% <100.00%> (-0.04%) ⬇️
OS-Linux 98.64% <100.00%> (-0.04%) ⬇️
OS-Windows 96.99% <100.00%> (-0.04%) ⬇️
OS-macOS 97.89% <100.00%> (-0.04%) ⬇️
Py-3.10 98.08% <100.00%> (-0.04%) ⬇️
Py-3.11 98.34% <100.00%> (-0.03%) ⬇️
Py-3.12 98.43% <100.00%> (-0.04%) ⬇️
Py-3.13 98.41% <100.00%> (-0.04%) ⬇️
Py-3.14 98.43% <100.00%> (-0.04%) ⬇️
Py-3.14t 97.52% <100.00%> (-0.04%) ⬇️
Py-pypy-3.11 97.36% <100.00%> (-0.04%) ⬇️
VM-macos 97.89% <100.00%> (-0.04%) ⬇️
VM-ubuntu 98.64% <100.00%> (-0.04%) ⬇️
VM-windows 96.99% <100.00%> (-0.04%) ⬇️
cython-coverage 37.97% <80.99%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

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

Comment thread aiohttp/http_base.py Outdated
import contextlib
import json
from http.cookies import SimpleCookie
from typing import Any, Callable, Optional, Tuple

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.

Callable, Optional, and Tuple are all deprecated.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Optional doesn't seem deprecated. Still, I will use [type] | None to be consistent with the rest of the code.

Comment thread aiohttp/http_base.py Outdated
Comment on lines +25 to +27
self._in_context = False
self._released: bool = False
self._resolve_charset: Callable[[Any, bytes], str] = lambda *_: "utf-8"

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.

The subclass doesn't call this. These could just be class defaults anyway, so don't need the init to define them.

Comment thread aiohttp/http_base.py
self._resolve_charset: Callable[[Any, bytes], str] = lambda *_: "utf-8"

# ----------------------------------------------------------------
# Abstract / overridable protocol methods

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 probably want to ensure these are defined, right? In which case this should probably be an abstract class and these methods can all be defined as abstract.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This requires multiple inheritance (HeadersMixin, ABC), but alright.

Comment thread aiohttp/http_base.py Outdated
Comment on lines +50 to +54
def headers(self) -> Any:
return self._headers

@property
def history(self) -> Tuple[Any, ...]:

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.

Seem to have lost typing information that we had before?

@Moist-Cat Moist-Cat Jul 28, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Changing it to tuple["BaseResponse", ...] breaks static analysis for functions that expect tuple["ClientResponse", ...]. In particular, ClientResponseError. I would rather be conservative and use Any than to change too many files and risk conflicts/rollbacks.
I ended up updating all references, in any case.

Comment thread aiohttp/client_reqrep.py Outdated


class ClientResponse(HeadersMixin):
class ClientResponse(BaseResponse):

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.

So this is the HTTP/1 class, right?

I still think, atleast in master/v4, that we probably want to rename this and have the base class be called ClientResponse. That means that middlewares etc. which reference ClientResponse shouldn't need any changes to work in future with the HTTP/2 class.

With these current changes, we'd need to changes lots of references from ClientResponse -> BaseResponse, both in our code and in user's code.

@Moist-Cat Moist-Cat Jul 28, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I still think, atleast in master/v4, that we probably want to rename this and have the base class be called ClientResponse

This causes type errors as well because we are reducing the capabilities of the ClientResponse class. All the methods that use the response as a connector object via start, read, etc, stop working. Though, reviewing the type errors, it seems that most come from the tests. I'm not sure if any users or middlewares use these methods.
That said, I'm okay with switching the child class with the new parent class if you believe it's better for backwards compatibility.

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.

Yeah, but that's the HTTP/1 specific methods, right? Any code that currently requires them is going to break regardless, as anything that currently receives ClientResponse, will in future need to handle receiving both HTTP1 and HTTP2 classes.

Code which doesn't depend on those methods will need no updates to annotations etc. if the base class reuses the existing ClientResponse name. So, yeah, I think it's a much easier migration for users if the parent is ClientResponse.

@Moist-Cat Moist-Cat Jul 28, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yeah, but that's the HTTP/1 specific methods, right? Any code that currently requires them is going to break regardless, as anything that currently receives ClientResponse, will in future need to handle receiving both HTTP1 and HTTP2 classes.

Yes, that's true.

Code which doesn't depend on those methods will need no updates to annotations etc. if the base class reuses the existing ClientResponse name. So, yeah, I think it's a much easier migration for users if the parent is ClientResponse.

That makes sense to me.

@Moist-Cat

Moist-Cat commented Jul 28, 2026

Copy link
Copy Markdown
Author

The main problem is that the code does not differentiate when the request object is used as a data structure and when it's used as a connector object that holds the current state of the connection. After finding more examples of this mixed behaviour it appears to me at the present that it would be difficult to implement HTTP/3 after HTTP/2 without adding a considerable code debt. Refactoring the response class to match the architecture I have in mind will require several small, incremental steps to avoid major breaking changes but it's better than a patchwork.

If you agree, I believe the best course of action would be to try to add HTTP/2 as an experimental feature after we finish with this and keep making small changes until we can support the three major HTTP versions without hacks or unexpected behaviour. The reason of why I think it's better to implement HTTP/2 first is to improve over something that works rather than changing the architecture without having a concrete implementation. Pushing the changes to a temporary developing branch first until the code is refactored is an option too but I'm not sure of how long will it take or if a clean solution is possible without major breaking changes and pushing many complex changes at once is never a good idea.

@Dreamsorcerer

Copy link
Copy Markdown
Member

If you agree, I believe the best course of action would be to try to add HTTP/2 as an experimental feature after we finish with this and keep making small changes until we can support the three major HTTP versions without hacks or unexpected behaviour. The reason of why I think it's better to implement HTTP/2 first is to improve over something that works rather than changing the architecture without having a concrete implementation. Pushing the changes to a temporary developing branch first until the code is refactored is an option too but I'm not sure of how long will it take or if a clean solution is possible without major breaking changes and pushing many complex changes at once is never a good idea.

OK, how about you start preparing these changes as a stacked PR? I'll ask for us to be opted-in to the stacked PRs beta, then you can create it.

That way you can show the full implementation, and we can merge the iterative steps as we go.

@Moist-Cat

Moist-Cat commented Jul 28, 2026

Copy link
Copy Markdown
Author

OK, how about you start preparing these changes as a stacked PR? I'll ask for us to be opted-in to the stacked PRs beta, then you can create it.

That way you can show the full implementation, and we can merge the iterative steps as we go.

Oh, this is the first time I hear about stacked PRs. Yes, that's perfect.

I will need some time to study the codebase and design an architecture that can accommodate the three protocols and doesn't break the public API so I will be switching between refactoring, and adapting the HTTP/2 and HTTP/1.1 implementations to the unified interface. If everything goes well, I might implement a minimal HTTP/3 connector while I'm at it (disabled, of course).

@Dreamsorcerer

Copy link
Copy Markdown
Member

I've just been told they have a lot of requests for the beta already, so not sure if we'll get added or not. Will update you if I hear back.

@Dreamsorcerer

Dreamsorcerer commented Jul 30, 2026

Copy link
Copy Markdown
Member

Whether due to my pestering or not, stacked PRs are now in public preview, so we can use them.

So, feel free to create a stacked PR. One thing to include in the full PR would be separate HTTP/2 codspeed benchmarks, so we can compare the performance between the HTTP/1 and HTTP/2 stacks.

Moist-Cat and others added 2 commits August 3, 2026 11:48
    HTTPResponse is the old ClientRequest that mixed presentation with
connection handling. In the case of the tests, HTTPResponse was aliased
to avoid conflicts.
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.

3 participants