diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index bf1463909c..cc5fd29dd3 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -7,10 +7,10 @@ name: "CodeQL" on: push: - branches: [master] + branches: [main] pull_request: # The branches below must be a subset of the branches above - branches: [master] + branches: [main] schedule: - cron: '0 23 * * 0' diff --git a/.github/workflows/lock-issues.yml b/.github/workflows/lock-issues.yml new file mode 100644 index 0000000000..d4a9e6478b --- /dev/null +++ b/.github/workflows/lock-issues.yml @@ -0,0 +1,18 @@ +name: 'Lock Threads' + +on: + schedule: + - cron: '0 0 * * *' + +permissions: + issues: write + pull-requests: write + +jobs: + action: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v2 + with: + issue-lock-inactive-days: 90 + pr-lock-inactive-days: 90 diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index cf5f0b4b91..b8e90c78c7 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -5,7 +5,7 @@ on: [push, pull_request] jobs: build: runs-on: ${{ matrix.os }} - + timeout-minutes: 10 strategy: fail-fast: false matrix: diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index 73d0b2931a..4e70af957c 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -37,12 +37,12 @@ get all the information we need from this object. Requests' simple API means that all forms of HTTP request are as obvious. For example, this is how you make an HTTP POST request:: - >>> r = requests.post('https://httpbin.org/post', data = {'key':'value'}) + >>> r = requests.post('https://httpbin.org/post', data={'key': 'value'}) Nice, right? What about the other HTTP request types: PUT, DELETE, HEAD and OPTIONS? These are all just as simple:: - >>> r = requests.put('https://httpbin.org/put', data = {'key':'value'}) + >>> r = requests.put('https://httpbin.org/put', data={'key': 'value'}) >>> r = requests.delete('https://httpbin.org/delete') >>> r = requests.head('https://httpbin.org/get') >>> r = requests.options('https://httpbin.org/get') diff --git a/requests/adapters.py b/requests/adapters.py index fa4d9b3cc9..9b7f92af66 100644 --- a/requests/adapters.py +++ b/requests/adapters.py @@ -19,6 +19,7 @@ from urllib3.exceptions import ClosedPoolError from urllib3.exceptions import ConnectTimeoutError from urllib3.exceptions import HTTPError as _HTTPError +from urllib3.exceptions import InvalidHeader as _InvalidHeader from urllib3.exceptions import MaxRetryError from urllib3.exceptions import NewConnectionError from urllib3.exceptions import ProxyError as _ProxyError @@ -37,7 +38,7 @@ from .cookies import extract_cookies_to_jar from .exceptions import (ConnectionError, ConnectTimeout, ReadTimeout, SSLError, ProxyError, RetryError, InvalidSchema, InvalidProxyURL, - InvalidURL) + InvalidURL, InvalidHeader) from .auth import _basic_auth_str try: @@ -527,6 +528,8 @@ def send(self, request, stream=False, timeout=None, verify=True, cert=None, prox raise SSLError(e, request=request) elif isinstance(e, ReadTimeoutError): raise ReadTimeout(e, request=request) + elif isinstance(e, _InvalidHeader): + raise InvalidHeader(e, request=request) else: raise diff --git a/tests/test_lowlevel.py b/tests/test_lowlevel.py index 4127fb115e..2ada004a74 100644 --- a/tests/test_lowlevel.py +++ b/tests/test_lowlevel.py @@ -24,6 +24,54 @@ def test_chunked_upload(): assert r.request.headers['Transfer-Encoding'] == 'chunked' +def test_chunked_encoding_error(): + """get a ChunkedEncodingError if the server returns a bad response""" + + def incomplete_chunked_response_handler(sock): + request_content = consume_socket_content(sock, timeout=0.5) + + # The server never ends the request and doesn't provide any valid chunks + sock.send(b"HTTP/1.1 200 OK\r\n" + + b"Transfer-Encoding: chunked\r\n") + + return request_content + + close_server = threading.Event() + server = Server(incomplete_chunked_response_handler) + + with server as (host, port): + url = 'http://{}:{}/'.format(host, port) + with pytest.raises(requests.exceptions.ChunkedEncodingError): + r = requests.get(url) + close_server.set() # release server block + + +def test_conflicting_content_lengths(): + """Ensure we correctly throw an InvalidHeader error if multiple + conflicting Content-Length headers are returned. + """ + + def multiple_content_length_response_handler(sock): + request_content = consume_socket_content(sock, timeout=0.5) + + sock.send(b"HTTP/1.1 200 OK\r\n" + + b"Content-Type: text/plain\r\n" + + b"Content-Length: 16\r\n" + + b"Content-Length: 32\r\n\r\n" + + b"-- Bad Actor -- Original Content\r\n") + + return request_content + + close_server = threading.Event() + server = Server(multiple_content_length_response_handler) + + with server as (host, port): + url = 'http://{}:{}/'.format(host, port) + with pytest.raises(requests.exceptions.InvalidHeader): + r = requests.get(url) + close_server.set() + + def test_digestauth_401_count_reset_on_redirect(): """Ensure we correctly reset num_401_calls after a successful digest auth, followed by a 302 redirect to another digest auth prompt.