Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

Prevent redirects on payment renewals and refreshes; stop funding on 3xx

Đang mở
#87 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 2 ngày

Chưa có ai nhận issue này.

Đánh giá

Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức phù hợp với người mới
45/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
python
Lĩnh vực
api, backend

Hướng nghiên cứu

Start with src/livepeer_gateway/http.py and src/livepeer_gateway/remote_signer.py to trace renewal and refresh requests through the HTTP helpers and funding loop. Add regression tests for redirect handling, terminal funding behavior, successful requests, and unchanged redirect behavior for other callers; update mock expectations as described. Run the SDK test suite and confirm 3xx responses do not reach redirect targets or trigger further ticket signing.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

Payment renewals currently follow redirects through _post_empty, and payment-parameter refreshes follow redirects through post_json. This proposal moves the fix to the SDK that owns those requests, following investigation in https://github.com/NE-Build-Track/livepeer-builder-core/issues/72

Problem

At 2017a80c740af5fcb0fef4ee84659af9b07fb90c, LivePaymentSession.send_payment() posts Livepeer-Payment and Livepeer-Segment through a non-stream HTTP path that follows redirects. Those custom headers can travel to the redirect target, and a downstream 2xx is treated as a successful renewal. The funding loop then requests another ticket on the next interval.

_refresh_payment_params() also follows redirects. A redirect target can supply the payment_params that the session uses for subsequent signing requests.

Both paths should refuse redirects, and a 3xx should stop funding instead of minting additional tickets. This can be implemented directly in the SDK's existing helpers and loop.

Proposal

This proposal addresses builder-core issue #72 in livepeer-python-gateway, against commit 2017a80.

The production patch adds 19 lines and removes 2 across two files, plus regression tests still to be added. It changes the existing HTTP helpers and payment loop directly.

Behavior

  • Renewal and refresh requests pass allow_redirects=False.
  • Any 3xx response on those requests raises LivepeerHTTPError immediately after the headers arrive, before reading the response body.
  • The existing run_payments() loop stops on 3xx, preventing additional tickets from being minted against a redirecting endpoint.
  • Existing handling of 404, other permanent 4xx, 408, 429, server errors, transport failures and cancellation is preserved.
  • Other callers retain their current redirect behavior. Existing stream redirect protection remains in effect.

Checking the status before reading the body prevents a truncated or stalled redirect body from turning a terminal redirect into a retryable transport error. Existing exception cleanup closes the response and session.

Changes

In remote_signer.py, disable redirects for send_payment() and _refresh_payment_params(). Extend the existing funding loop's terminal-status range from 400 <= status < 500 to 300 <= status < 500, retaining the exceptions for 408 and 429.

In http.py, add an optional allow_redirects=True argument and forward it through the existing paths:

_post_empty → _request_body → _open_request
post_json → request_json → _request_body → _open_request

In _open_request(), apply allow_redirects=False when requested and reject 3xx before reading the body. Only set the option when false so existing stream-specific redirect restrictions are preserved.

Complete production patch

The complete proposed production patch is embedded below.

--- a/src/livepeer_gateway/http.py
+++ b/src/livepeer_gateway/http.py
@@ -146,6 +146,7 @@
     payload: RunnerPayload | _StreamState = None,
     headers: dict[str, str] | None = None,
     timeout: float = 5.0,
+    allow_redirects: bool = True,
 ) -> tuple[bytes, str]:
     """
     Make an HTTP request and return the raw response body.
@@ -161,6 +162,7 @@
         session, resp = await _open_request(
             url, method=method, payload=payload, headers=headers,
             timeout=aiohttp.ClientTimeout(total=timeout), handshake_timeout=timeout,
+            allow_redirects=allow_redirects,
         )
         async with session:
             try:
@@ -205,6 +207,7 @@
     payload: dict[str, Any] | None = None,
     headers: dict[str, str] | None = None,
     timeout: float = 5.0,
+    allow_redirects: bool = True,
 ) -> Any:
     """
     Make an async JSON HTTP request and parse the JSON response.
@@ -219,6 +222,7 @@
         payload=payload,
         headers=headers,
         timeout=timeout,
+        allow_redirects=allow_redirects,
     )
     try:
         return json.loads(raw)
@@ -264,15 +268,20 @@
     headers: dict[str, str] | None,
     timeout: aiohttp.ClientTimeout,
     handshake_timeout: float,
+    allow_redirects: bool = True,
 ) -> tuple[aiohttp.ClientSession, aiohttp.ClientResponse]:
     method, headers, body = _request_parts(url, method=method, payload=payload, headers=headers)
     session, options = _request_session(payload, timeout, handshake_timeout)
+    if not allow_redirects:
+        options["allow_redirects"] = False
     if payload is None:
         # aiohttp otherwise adds application/octet-stream even for an empty POST.
         options["skip_auto_headers"] = {"Content-Type"}
     resp = None
     try:
         resp = await session.request(method, url, data=body, headers=headers, **options)
+        if not allow_redirects and 300 <= resp.status < 400:
+            _raise_http_json_error(resp.status, url)
         if _request_failed(resp.status, payload):
             # Read before cancelling the writer: aiohttp 3.13 closes the socket
             # on cancellation, which can discard a fragmented payment challenge.
@@ -296,6 +305,7 @@
     *,
     headers: dict[str, str] | None = None,
     timeout: float = 5.0,
+    allow_redirects: bool = True,
 ) -> dict[str, Any]:
     """
     POST JSON to `url` and parse a JSON object response.
@@ -305,6 +315,7 @@
         payload=payload,
         headers=headers,
         timeout=timeout,
+        allow_redirects=allow_redirects,
     )
     return _ensure_json_object(data, url=url)
 
@@ -326,6 +337,7 @@
     *,
     headers: dict[str, str] | None = None,
     timeout: float = 5.0,
+    allow_redirects: bool = True,
 ) -> None:
     """POST an empty body to ``url`` and discard the response."""
     await _request_body(
@@ -333,6 +345,7 @@
         method="POST",
         headers=headers,
         timeout=timeout,
+        allow_redirects=allow_redirects,
     )
 
 
--- a/src/livepeer_gateway/remote_signer.py
+++ b/src/livepeer_gateway/remote_signer.py
@@ -170,7 +170,9 @@
             "Livepeer-Payment": payment.payment,
             "Livepeer-Segment": payment.seg_creds,
         }
-        await _post_empty(self._challenge.payment_url, headers=headers, timeout=5.0)
+        await _post_empty(
+            self._challenge.payment_url, headers=headers, timeout=5.0, allow_redirects=False,
+        )
 
     async def run_payments(self) -> bool:
         """Keep a metered session funded until cancelled or the session ends.
@@ -186,10 +188,11 @@
             except SkipPaymentCycle as e:
                 _LOG.debug("Payment loop skipped cycle: %s", e)
             except LivepeerHTTPError as e:
+                # A redirect or permanent 4xx stops funding.
                 # A 4xx will not change on a retry (404 gone, 409 fixed price,
                 # 403 mismatch), so stop rather than mint tickets nobody will
                 # honour. 408 and 429 are the two that do ask to be retried.
-                if 400 <= e.status_code < 500 and e.status_code not in (408, 429):
+                if 300 <= e.status_code < 500 and e.status_code not in (408, 429):
                     _LOG.info("Payment loop stopping (HTTP %d): %s", e.status_code, e)
                     return e.status_code == 404
                 _LOG.warning("Payment failed; retrying next cycle: %s", e)
@@ -249,6 +252,7 @@
                 "sender": signer.address,
                 "manifest_id": self._challenge.manifest_id,
             },
+            allow_redirects=False,
         )
         payment_params = data.get("payment_params")
         if not isinstance(payment_params, str) or not payment_params:

Validation and remaining tests

The proposed code was validated in memory with 22 loopback checks, including renewal and refresh redirects, a truncated 307 response, successful refreshes, retry behavior and the default redirect behavior of other callers. The patch passes git apply --check against 2017a80.

Before merging, add permanent regression tests and run the full SDK suite. Update existing mock expectations to include allow_redirects=False on payment requests. Coverage should verify:

  • 301, 302, 303, 307 and 308 renewals reach no redirect target and stop further signing.
  • Redirected refreshes reach no target and never replace the original payment parameters.
  • A truncated or stalled 3xx body cannot trigger another payment cycle.
  • Successful renewals and refreshes still work.
  • Existing terminal-status, retry and cancellation behavior is preserved.
  • Unrelated HTTP requests retain their existing redirect behavior.

No repository source files were changed during validation. This is a proposed production patch, not a complete, tested upstream PR.

Effect on builder-core

Once builder-core consumes an SDK release or commit containing this fix, it can retain its original SDK funding path instead of a new payment module and replacement funding loop. Refresh hardening and terminal redirect handling would already be implemented in the SDK.

Consuming this upstream fix requires updating builder-core's SDK pin. Changing upstream source cannot alter the behavior of the currently pinned commit.

Ngôn ngữ chính
Python
Star
1
Fork
10
Merge trung bình
1 ngày 9 giờ
Pull request đã merge (30 ngày)
7

Chuẩn bị môi trường

Dự án này không cung cấp dev container, Dockerfile hay hướng dẫn đóng góp, nên bạn cần tự thiết lập môi trường: hãy bắt đầu từ README và xem hướng dẫn đóng góp lần đầu của chúng tôi để biết các bước chung.

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của livepeer/livepeer-python-gateway

Tất cả issue của livepeer/livepeer-python-gateway

Issue tương tự

Thêm issue về Python

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.