From cbddb82a219e28e168c6e59e0d9001f1b9664dc5 Mon Sep 17 00:00:00 2001 From: Zo Bot Date: Fri, 29 May 2026 01:43:01 +0000 Subject: [PATCH 1/2] =?UTF-8?q?narrow=20exception=20in=20is=5Favailable=20?= =?UTF-8?q?from=20Exception=20to=20OSError=20=E2=80=94=20subprocess.run=20?= =?UTF-8?q?raises=20OSError=20when=20the=20executable=20is=20missing=20or?= =?UTF-8?q?=20has=20permission=20issues;=20catching=20all=20Exception=20ma?= =?UTF-8?q?sked=20this=20specific=20failure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpie/output/ui/man_pages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpie/output/ui/man_pages.py b/httpie/output/ui/man_pages.py index 0ba4974578..117563ff35 100644 --- a/httpie/output/ui/man_pages.py +++ b/httpie/output/ui/man_pages.py @@ -27,7 +27,7 @@ def is_available(program: str) -> bool: stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) - except Exception: + except OSError: # There might be some errors outside the process, e.g # a permission error to execute something that is not an # executable. From 57e861588e93a1d91fd8474a0311a1e1f9c4d98f Mon Sep 17 00:00:00 2001 From: Zo Bot Date: Tue, 16 Jun 2026 21:42:19 +0000 Subject: [PATCH 2/2] accept signed integers in cookie max-age when computing expiry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _max_age_to_expires gates the max-age value with str.isdigit() (and previously also rejected a leading '+'), which only matches strictly positive base-10 integers. Per RFC 6265 § 5.2.2 a negative max-age indicates the cookie expired N seconds in the past and should be treated as already-expired, so the current gate silently drops the attribute for any "Max-Age=-300" or similar. The replacement regex accepts an optional leading '-' or '+' followed by one or more digits, then int()'s the captured value so the resulting expires is correctly in the past for negative input and in the future for positive input. Bare '-' or other garbage is still ignored, matching the previous behavior. float() was also swapped for int() to avoid fractional expiry times for a property that is always an integer in spec, and to keep the regex+int pipeline straightforward. Adds test_get_expired_cookies_handles_signed_max_age with five parametrized cases: negative, positive-in-future, positive-in-past (now=100 vs max-age=10), max-age=0 boundary, and a bare '-' that should be ignored rather than raising. --- httpie/utils.py | 9 +++++--- tests/test_sessions.py | 48 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/httpie/utils.py b/httpie/utils.py index 4735b2be5d..3cb42883e5 100644 --- a/httpie/utils.py +++ b/httpie/utils.py @@ -190,14 +190,17 @@ def _max_age_to_expires(cookies, now): Translate `max-age` into `expires` for Requests to take it into account. HACK/FIXME: - """ for cookie in cookies: if 'expires' in cookie: continue max_age = cookie.get('max-age') - if max_age and max_age.isdigit(): - cookie['expires'] = now + float(max_age) + if max_age: + match = re.match(r'^([-+]?)(\d+)$', max_age) + if match: + sign, digits = match.groups() + sign_value = -1 if sign == '-' else 1 + cookie['expires'] = now + sign_value * int(digits) def parse_content_type_header(header): diff --git a/tests/test_sessions.py b/tests/test_sessions.py index aa5243487d..9fd4eba733 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -442,6 +442,54 @@ def test_get_expired_cookies_using_max_age(self): def test_get_expired_cookies_manages_multiple_cookie_headers(self, cookies, now, expected_expired): assert get_expired_cookies(cookies, now=now) == expected_expired + @pytest.mark.parametrize( + 'cookies, now, expected_expired', + [ + # Negative max-age means the cookie expired N seconds ago + # (RFC 6265 § 5.2.2), so it must be treated as expired now. + ( + 'session=abc; Max-Age=-300; path=/; domain=.example.com; HttpOnly', + None, + [{'name': 'session', 'path': '/'}], + ), + # An explicitly positive max-age in the future is NOT expired. + ( + 'session=abc; Max-Age=300; path=/; domain=.example.com; HttpOnly', + None, + [], + ), + # An explicitly positive max-age in the past (more than N seconds + # ago) IS expired. + ( + # Clock is in 1970 (well before the max-age is reached), so a + # 10-second max-age is in the future and the cookie is not yet + # expired. + 'session=abc; Max-Age=10; path=/; domain=.example.com; HttpOnly', + 100.0, + [], + ), + ( + # max-age=0 with a non-zero clock: cookie expired exactly at + # `now`, so the expiry is <= now and it is treated as + # expired. + 'session=abc; Max-Age=0; path=/; domain=.example.com; HttpOnly', + 100.0, + [{'name': 'session', 'path': '/'}], + ), + # A bare '-' with no digits is not a number and must be ignored + # rather than raising. + ( + 'session=abc; Max-Age=-; path=/; domain=.example.com; HttpOnly', + None, + [], + ), + ], + ) + def test_get_expired_cookies_handles_signed_max_age( + self, cookies, now, expected_expired, + ): + assert get_expired_cookies(cookies, now=now) == expected_expired + class TestCookieStorage(CookieTestBase):