Repository navigation
gh-49288: Reset file-like POST body on HTTP auth retry #153726
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
base: main
Are you sure you want to change the base?
Changes from all commits
3e3ae2b
eefaab7
a756366
945a7ce
8bc5f22
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -920,6 +920,29 @@ def is_authenticated(self, authuri): | |
| return self.authenticated[uri] | ||
|
|
||
|
|
||
| class _AuthBodyReiterator: | ||
| """Re-read a seekable file-like POST body from the start.""" | ||
|
|
||
| def __init__(self, fileobj, blocksize=8192): | ||
| self._fileobj = fileobj | ||
| self._blocksize = blocksize | ||
|
|
||
| def __iter__(self): | ||
| self._fileobj.seek(0) | ||
| while chunk := self._fileobj.read(self._blocksize): | ||
| yield chunk | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yielding raw chunks for files open in text-mode can fail. That's not what http.client is doing here. |
||
|
|
||
|
|
||
| def _prepare_auth_retry_body(request): | ||
| """Allow authentication retries to resend a consumed file-like object.""" | ||
| data = request.data | ||
| if data is None or not hasattr(data, 'read'): | ||
| return None | ||
| if not getattr(data, 'seekable', lambda: hasattr(data, 'seek'))(): | ||
| raise ValueError("seekable file-like Request.data is required when " | ||
| "the request may be retried (such as HTTP authentication retries).") | ||
| request.data = _AuthBodyReiterator(data) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Swapping |
||
|
|
||
| class AbstractBasicAuthHandler: | ||
|
|
||
| # XXX this allows for multiple auth-schemes, but will stupidly pick | ||
|
|
@@ -1001,6 +1024,7 @@ def retry_http_basic_auth(self, host, req, realm): | |
| auth = "Basic " + base64.b64encode(raw.encode()).decode("ascii") | ||
| if req.get_header(self.auth_header, None) == auth: | ||
| return None | ||
| _prepare_auth_retry_body(req) | ||
| req.add_unredirected_header(self.auth_header, auth) | ||
| return self.parent.open(req, timeout=req.timeout) | ||
| else: | ||
|
|
@@ -1113,6 +1137,8 @@ def retry_http_digest_auth(self, req, auth): | |
| auth_val = 'Digest %s' % auth | ||
| if req.headers.get(self.auth_header, None) == auth_val: | ||
| return None | ||
|
|
||
| _prepare_auth_retry_body(req) | ||
| req.add_unredirected_header(self.auth_header, auth_val) | ||
| resp = self.parent.open(req, timeout=req.timeout) | ||
| return resp | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| Fix :mod:`urllib.request` HTTP authentication retries sending an empty POST | ||
| body when :attr:`~urllib.request.Request.data` is a consumed file-like | ||
| object. The body is now re-read from the start on retry; non-seekable | ||
| streams raise :exc:`ValueError`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The retry rewinds to offset 0, not to where the body started, so it can send different bytes. Try:
This is a regression compared to sending an empty response because the latter usually gets rejected.