Prevent redirects on payment renewals and refreshes; stop funding on 3xx
维护者通常 2 天内回复
还没有人认领这个 Issue。
评估
调研方向
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.
由索引模型根据 Issue 内容生成。
描述
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.
- 主要语言
- Python
- 星标
- 1
- 派生
- 10
- 平均合并
- 1 天 9 小时
- 30 天内合并 PR
- 7
环境准备
这个项目没有提供开发容器、Dockerfile 或贡献指南,环境需要你自己搭建:先看它的 README,通用步骤见我们的新手贡献指南。
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
livepeer/livepeer-python-gateway 的其他 Issue
-
call_runner rejects a top-level JSON array, so a runner cannot pass one through可能已有人在做 @rickstaa 于 74 天前认领。 未关闭
难度 2/5 1-3 小时 新手友好度 78/100
livepeer/livepeer-python-gateway#64 · 1 条评论 ·
维护者通常 2 天内回复
-
Align pricing terminology: pixels_per_unit -> pricing_unit_size (track go-livepeer #3942)可能重新可做 关联的 PR 已关闭且未合并。 未关闭Improvement
难度 1/5 1 小时以内 新手友好度 68/100
livepeer/livepeer-python-gateway#27 · 1 条评论 ·
维护者通常 2 天内回复
-
难度 2/5 1-3 小时 新手友好度 76/100
livepeer/livepeer-python-gateway#21 ·
维护者通常 2 天内回复
-
trickle_publisher: `_resolve_next_seq` fallback drops first video segment + logs noisy warning可能重新可做 关联的 PR 已关闭且未合并。 未关闭
难度 1/5 1 小时以内 新手友好度 70/100
livepeer/livepeer-python-gateway#12 · 1 条评论 ·
维护者通常 2 天内回复
-
难度 3/5 1-2 天 新手友好度 62/100
livepeer/livepeer-python-gateway#88 ·
维护者通常 2 天内回复
查看 livepeer/livepeer-python-gateway 的全部 Issue
相似的 Issue
-
bug
难度 2/5 1-3 小时 新手友好度 74/100
BCDA-APS/polar-bits#93 ·
-
infer_audio_type only recognises one MPEG Layer III frame header, so most MP3 files are rejected可能已有人在做 @Steve-20-ai 今天认领。 未关闭
难度 2/5 1-3 小时 新手友好度 78/100
维护者通常 1 天内回复
-
[BUG] - ssh_config `Match tagged` statement always applies可能已有人在做 @AtharvRG 今天认领。 未关闭Bug
难度 2/5 1-3 小时 新手友好度 68/100
paramiko/paramiko#2715 · 1 条评论 ·
维护者通常 9 天内回复
-
auto-rejected intake mcp-intake needs-salvage priority:high type:feature
难度 2/5 1-3 小时 新手友好度 65/100
Ikalus1988/MisakaNet#4221 · 2 条评论 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 82/100
维护者通常 1 天内回复