Hacktoberfest 2026 : les issues que les mainteneurs ont marquées pour octobre, ouvertes et accessibles aux débutants. Parcourir les issues Hacktoberfest

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

Ouverte
#87 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Les mainteneurs répondent en général sous 2 jours

Personne n'a encore pris cette issue.

Évaluation

Difficulté
4/5
Temps estimé
3-5 jours
Accessibilité débutants
45/100
Type d'issue
Bug
Clarté
Clairement spécifiée
Activité
Active
Stack technique
python
Domaine
api, backend

Piste de recherche

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.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Description

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.

Langage dominant
Python
Étoiles
1
Forks
10
Merge moyen
1 j 9 h
PR mergées (30 j)
7

Préparer son environnement

Ce projet ne fournit ni conteneur de développement, ni Dockerfile, ni guide de contribution : l'installation est à votre charge. Commencez par son README, et consultez notre guide de la première contribution pour les étapes générales.

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de livepeer/livepeer-python-gateway

Toutes les issues de livepeer/livepeer-python-gateway

Issues similaires

Plus d'issues Python

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.