-
Notifications
You must be signed in to change notification settings - Fork 648
fix(proxy): pass clientAbort.signal to asset download fetches + abort guard on chat /imagegen catch #276
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(proxy): pass clientAbort.signal to asset download fetches + abort guard on chat /imagegen catch #276
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2645,7 +2645,7 @@ export async function startProxy(options: ProxyOptions): Promise<ProxyHandle> { | |
| console.log(`[ClawRouter] Image saved → ${img.url}`); | ||
| } else if (img.url?.startsWith("https://") || img.url?.startsWith("http://")) { | ||
| try { | ||
| const imgResp = await fetch(img.url); | ||
| const imgResp = await fetch(img.url, { signal: clientAbort.signal }); | ||
| if (imgResp.ok) { | ||
| const contentType = imgResp.headers.get("content-type") ?? "image/png"; | ||
| const ext = | ||
|
|
@@ -2697,6 +2697,10 @@ export async function startProxy(options: ProxyOptions): Promise<ProxyHandle> { | |
| // Accepts image as: data URI, local file path, ~/path, or HTTP(S) URL | ||
| if (req.url === "/v1/images/image2image" && req.method === "POST") { | ||
| const img2imgStartTime = Date.now(); | ||
| const clientAbort = new AbortController(); | ||
| res.on("close", () => { | ||
| if (!res.writableEnded) clientAbort.abort(); | ||
| }); | ||
| const chunks: Buffer[] = []; | ||
| for await (const chunk of req) { | ||
| chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); | ||
|
|
@@ -2718,7 +2722,7 @@ export async function startProxy(options: ProxyOptions): Promise<ProxyHandle> { | |
| // Already a data URI — pass through | ||
| } else if (val.startsWith("https://") || val.startsWith("http://")) { | ||
| // Download URL → data URI | ||
| const imgResp = await fetch(val); | ||
| const imgResp = await fetch(val, { signal: clientAbort.signal }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- applicable repository guidance ---'
head -5 /tmp/coderabbit-repo-knowledge/blockrunai-clawrouter-816fc52c/*/*.md 2>/dev/null || true
printf '%s\n' '--- src/proxy.ts relevant source ---'
sed -n '2640,2805p' src/proxy.ts
printf '%s\n' '--- changed hunks in the relevant area ---'
git diff --unified=35 -- src/proxy.ts | sed -n '/image2image/,+180p'Repository: BlockRunAI/ClawRouter Length of output: 10162 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- image2image completion and outer catch ---'
sed -n '2760,2865p' src/proxy.ts
printf '%s\n' '--- payFetch binding and implementation ---'
rg -n -C 12 'createPayFetchWithPreAuth|const payFetch|function payFetch|payFetch =' src
printf '%s\n' '--- relevant tests and project runtime contract ---'
rg -n -C 8 'image2image|clientAbort|createPayFetchWithPreAuth|signal:' src test tests package.json 2>/dev/null || trueRepository: BlockRunAI/ClawRouter Length of output: 50377 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- payment-preauth request flow ---'
sed -n '49,190p' src/payment-preauth.ts
printf '%s\n' '--- exact diff for the cancellation change ---'
git diff --unified=12 -- src/proxy.ts | rg -n -C 20 'image2image|clientAbort|signal'Repository: BlockRunAI/ClawRouter Length of output: 5751 Guard image2image cancellation before error handling. If 🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same rationale as the other two this PR adds the missing signals to match the existing pattern. The catch-path guards are a valid follow-up but out of scope here. The img2img outer catch already has There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- version references ---'
rg -n -C 2 '0\.12\.252|VERSION' src package.json 2>/dev/null || true
echo '--- image2image handler catch paths ---'
sed -n '2685,2860p' src/proxy.ts
echo '--- abort guards in image2image handler region ---'
sed -n '2685,2860p' src/proxy.ts | rg -n -C 3 'clientAbort\.signal\.aborted|catch \(' || trueLength of output: 17545
The The I agree that this is a follow-up concern and is outside this PR's stated scope. Would you like me to open a GitHub issue for the two 🐇 You are interacting with an AI system.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, go ahead and open the issue. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
repo="BlockRunAI/ClawRouter"
title="Guard image2image abort errors after client disconnect"
existing_url="$(
gh issue list \
--repo "$repo" \
--state open \
--search "\"$title\" in:title" \
--json title,url \
--jq ".[] | select(.title == \"$title\") | .url" \
| head -n 1
)"
if [ -n "$existing_url" ]; then
printf 'EXISTING_ISSUE=%s\n' "$existing_url"
exit 0
fi
issue_body="$(cat <<'EOF'
## Summary
Add client-abort guards to the `/v1/images/image2image` error paths in `src/proxy.ts`.
## Rationale
The handler now passes `clientAbort.signal` to source and mask URL downloads and to the upstream `payFetch` request. When the client disconnects, either operation can reject with an abort error.
The parse catch currently treats an aborted URL download as invalid input and can try to write a `400` response. The outer catch can log the abort and try to write a `502` response. The closed response must not receive either error response.
## Required changes
- In the parse catch that resolves `image` and `mask` input URLs, return immediately when `clientAbort.signal.aborted`.
- In the outer catch around the upstream image2image request, return immediately when `clientAbort.signal.aborted`.
- Preserve the current `400` and `502` behavior for non-abort errors.
- Add Vitest coverage for client disconnect during:
- source or mask URL download
- upstream `payFetch`
## Acceptance criteria
- A client disconnect during either path does not log an image-editing error.
- A client disconnect during either path does not call `res.writeHead()` or `res.end()` to send an error response.
- Non-abort failures retain their current error responses.
## Backlinks
- Follow-up from PR `#276`: https://github.com/BlockRunAI/ClawRouter/pull/276
- Review comment: https://github.com/BlockRunAI/ClawRouter/pull/276#discussion_r3887529499
- Requested by: `@Sertug17`
EOF
)"
issue_url="$(
gh issue create \
--repo "$repo" \
--title "$title" \
--body "$issue_body" \
--assignee "Sertug17"
)"
printf 'CREATED_ISSUE=%s\n' "$issue_url"Length of output: 2310
The issue links to PR 🐇 You are interacting with an AI system. |
||
| if (!imgResp.ok) | ||
| throw new Error(`Failed to download ${field} from ${val}: HTTP ${imgResp.status}`); | ||
| const contentType = imgResp.headers.get("content-type") ?? "image/png"; | ||
|
|
@@ -2750,6 +2754,7 @@ export async function startProxy(options: ProxyOptions): Promise<ProxyHandle> { | |
| method: "POST", | ||
| headers: { "content-type": "application/json", "user-agent": USER_AGENT }, | ||
| body: reqBody, | ||
| signal: clientAbort.signal, | ||
| }); | ||
| const text = await upstream.text(); | ||
| if (!upstream.ok) { | ||
|
|
@@ -2781,7 +2786,7 @@ export async function startProxy(options: ProxyOptions): Promise<ProxyHandle> { | |
| console.log(`[ClawRouter] Image saved → ${img.url}`); | ||
| } else if (img.url?.startsWith("https://") || img.url?.startsWith("http://")) { | ||
| try { | ||
| const imgResp = await fetch(img.url); | ||
| const imgResp = await fetch(img.url, { signal: clientAbort.signal }); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| if (imgResp.ok) { | ||
| const contentType = imgResp.headers.get("content-type") ?? "image/png"; | ||
| const ext = | ||
|
|
@@ -3072,7 +3077,7 @@ export async function startProxy(options: ProxyOptions): Promise<ProxyHandle> { | |
| for (const clip of finalResult.data) { | ||
| if (clip.url?.startsWith("https://") || clip.url?.startsWith("http://")) { | ||
| try { | ||
| const videoResp = await fetch(clip.url); | ||
| const videoResp = await fetch(clip.url, { signal: clientAbort.signal }); | ||
| if (videoResp.ok) { | ||
| const contentType = videoResp.headers.get("content-type") ?? "video/mp4"; | ||
| const ext = contentType.includes("webm") | ||
|
|
@@ -3976,6 +3981,10 @@ async function proxyRequest( | |
| console.log( | ||
| `[ClawRouter] /imagegen command → ${imageModel} (${imageSize}): ${imagePrompt.slice(0, 80)}...`, | ||
| ); | ||
| const imagegenAbort = new AbortController(); | ||
| res.on("close", () => { | ||
| if (!res.writableEnded) imagegenAbort.abort(); | ||
| }); | ||
| try { | ||
| const imageUpstreamUrl = `${apiBase}/v1/images/generations`; | ||
| const imageBody = JSON.stringify({ | ||
|
|
@@ -3988,6 +3997,7 @@ async function proxyRequest( | |
| method: "POST", | ||
| headers: { "content-type": "application/json", "user-agent": USER_AGENT }, | ||
| body: imageBody, | ||
| signal: imagegenAbort.signal, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Propagate cancellation to the
🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed this is a real gap, but out of scope for this PR. The upload path exists in the current codebase without any abort signal, so this PR doesn't regress it. Can address in a follow up. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Would you like me to open a GitHub issue for the You are interacting with an AI system. |
||
| }); | ||
|
|
||
| const imageResult = (await imageResponse.json()) as { | ||
|
|
@@ -4086,6 +4096,7 @@ async function proxyRequest( | |
| ); | ||
| } | ||
| } catch (err) { | ||
| if (imagegenAbort.signal.aborted) return; // client gone — nothing to report | ||
| const errMsg = err instanceof Error ? err.message : String(err); | ||
| console.error(`[ClawRouter] /imagegen error: ${errMsg}`); | ||
| if (!res.headersSent) { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Stop asset processing after an abort.
The new signal cancels a pending
fetch, but the inner download catch treats the abort as a normal failure. If the disconnect occurs afterarrayBuffer()resolves or beforewriteFile, the handler can still write the asset and continue to the response path. CheckclientAbort.signal.abortedbefore each disk write and return immediately when the caught error is caused by client cancellation.Proposed guard
Also applies to: 3075-3075
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Valid point, but intentionally out of scope this PR strictly adds the missing
signalarguments to match the existing pattern (e.g. audio download at line 3016). Happy to follow up in a separate PR if maintainers want the inner abort guards too.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Sertug17, understood. The current change is consistent with the existing asset-download pattern and keeps this PR focused.The inner abort guards remain a valid follow-up. Would you like me to open a GitHub issue for
src/proxy.tsto track guards beforewriteFileand in the image and video download catch blocks?You are interacting with an AI system.