Prevent redirects on payment renewals and refreshes; stop funding on 3xx
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
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
LivepeerHTTPErrorimmediately 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
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Autres issues de livepeer/livepeer-python-gateway
-
call_runner rejects a top-level JSON array, so a runner cannot pass one throughPeut-être pris @rickstaa l’a pris il y a 74 jours. Ouverte
Difficulté 2/5 1-3 heures Accessibilité débutants 78/100
livepeer/livepeer-python-gateway#64 · 1 commentaire ·
Les mainteneurs répondent en général sous 2 jours
-
Align pricing terminology: pixels_per_unit -> pricing_unit_size (track go-livepeer #3942)Peut-être à nouveau libre Une pull request pour cette issue a été fermée sans être fusionnée. OuverteImprovement
Difficulté 1/5 Moins d'une heure Accessibilité débutants 68/100
livepeer/livepeer-python-gateway#27 · 1 commentaire ·
Les mainteneurs répondent en général sous 2 jours
-
Difficulté 2/5 1-3 heures Accessibilité débutants 76/100
livepeer/livepeer-python-gateway#21 ·
Les mainteneurs répondent en général sous 2 jours
-
trickle_publisher: `_resolve_next_seq` fallback drops first video segment + logs noisy warningPeut-être à nouveau libre Une pull request pour cette issue a été fermée sans être fusionnée. Ouverte
Difficulté 1/5 Moins d'une heure Accessibilité débutants 70/100
livepeer/livepeer-python-gateway#12 · 1 commentaire ·
Les mainteneurs répondent en général sous 2 jours
-
Difficulté 3/5 1-2 jours Accessibilité débutants 62/100
livepeer/livepeer-python-gateway#88 ·
Les mainteneurs répondent en général sous 2 jours
Toutes les issues de livepeer/livepeer-python-gateway
Issues similaires
-
Difficulté 2/5 1-3 heures Accessibilité débutants 82/100
Les mainteneurs répondent en général sous 1 jour
-
Claiming namespace `thejakia1`Ouvertenamespace operations
Difficulté 1/5 Moins d'une heure Accessibilité débutants 65/100
EclipseFdn/open-vsx.org#15057 ·
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 2/5 1-3 heures Accessibilité débutants 76/100
FinanceFlash/unvibecode#349 ·
-
Mission statement: why this is free, in the README, Why This Exists and the wiki HomePeut-être pris @ChiefGyk3D l’a pris aujourd’hui. Ouverte
Difficulté 2/5 1-3 heures Accessibilité débutants 76/100
ChiefGyk3D/Clouds-Against-Humanity#348 ·
Les mainteneurs répondent en général sous 1 jour
-
[Perf] Default --vllm-watermark 0.05 for rollouts (hybrid models re-preempt under KV pressure)Ouverte
Difficulté 2/5 1-3 heures Accessibilité débutants 74/100
vllm-project/vime#508 ·
Les mainteneurs répondent en général sous 4 jours