Follow redirects when downloading AI vision images #107

Manually merged
marvin8 merged 7 commits from fix/issue-105-follow-media-proxy-redirects into main 2026-09-08 04:37:07 +00:00
Collaborator

Problem

Posts whose attachments are served through Mastodon's media_proxy URLs never received a vision verdict: the URL answers 302 Found, and httpx2's raise_for_status() raises on 3xx responses, so the image download failed and the post stayed AI: pending for manual review (affected: posts 42, 43, 44, 46).

Fix

  • classify_posts creates the image-download client with follow_redirects=True.
  • download_first_image now guesses the MIME type from the post-redirect final URL (response.url), so proxied PNG/WebP/GIF attachments are labelled correctly instead of always falling back to image/jpeg.

Also on this branch

  • Dependency + pre-commit hook bump (root and both packages, uv.lock/pylock.toml in sync). The newer ty flags asyncio.iscoroutinefunction as deprecated, so this PR also swaps both zhongli/circuit_breaker.py call sites to inspect.iscoroutinefunction (behaviour-equivalent on Python 3.12+; existing sync/async circuit-breaker tests cover it).
  • Flaky log-capture tests (#110): root cause was alembic/env.py calling fileConfig with its default disable_existing_loggers=True, which set disabled=True on existing loggers and silently suppressed their records — intermittently breaking test_main/test_startup capture tests depending on worker order, and dropping the "Database migrations applied" log line in production. Fixed with disable_existing_loggers=False plus hermetic restore of logger.disabled in the capture helpers.

Testing

  • New end-to-end test: 302 → 200 through classify_posts via MockTransport; fails (red) without the fix, reproducing the production HTTPStatusError.
  • New MIME test: proxy URL redirecting to a .png final URL yields image/png.
  • Full DoD green for both packages: ruff, ruff format, ty (0 diagnostics), complexipy, tryke; fenliu-tryke-3.14 run 8× consecutively clean after the #110 fix (previously flaked ~1-in-3).

Closes #105
Closes #110

## Problem Posts whose attachments are served through Mastodon's `media_proxy` URLs never received a vision verdict: the URL answers `302 Found`, and httpx2's `raise_for_status()` raises on 3xx responses, so the image download failed and the post stayed `AI: pending` for manual review (affected: posts 42, 43, 44, 46). ## Fix - `classify_posts` creates the image-download client with `follow_redirects=True`. - `download_first_image` now guesses the MIME type from the post-redirect final URL (`response.url`), so proxied PNG/WebP/GIF attachments are labelled correctly instead of always falling back to `image/jpeg`. ## Also on this branch - **Dependency + pre-commit hook bump** (root and both packages, `uv.lock`/`pylock.toml` in sync). The newer `ty` flags `asyncio.iscoroutinefunction` as deprecated, so this PR also swaps both `zhongli/circuit_breaker.py` call sites to `inspect.iscoroutinefunction` (behaviour-equivalent on Python 3.12+; existing sync/async circuit-breaker tests cover it). - **Flaky log-capture tests (#110)**: root cause was `alembic/env.py` calling `fileConfig` with its default `disable_existing_loggers=True`, which set `disabled=True` on existing loggers and silently suppressed their records — intermittently breaking `test_main`/`test_startup` capture tests depending on worker order, and dropping the "Database migrations applied" log line in production. Fixed with `disable_existing_loggers=False` plus hermetic restore of `logger.disabled` in the capture helpers. ## Testing - New end-to-end test: 302 → 200 through `classify_posts` via `MockTransport`; fails (red) without the fix, reproducing the production `HTTPStatusError`. - New MIME test: proxy URL redirecting to a `.png` final URL yields `image/png`. - Full DoD green for both packages: ruff, ruff format, ty (0 diagnostics), complexipy, tryke; `fenliu-tryke-3.14` run 8× consecutively clean after the #110 fix (previously flaked ~1-in-3). Closes #105 Closes #110
🐛 (fenliu) follow redirects when downloading media_proxy images
All checks were successful
/ gitleaks (pull_request) Successful in 11s
/ checks (pull_request) Successful in 2m44s
/ publish (pull_request) Has been skipped
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 4m57s
789dffa770
httpx2 raises on 3xx responses, so the vision stage's image download
failed for posts whose attachments are served via Mastodon's
media_proxy redirect URLs. Create the download client with
follow_redirects=True, and guess the MIME type from the post-redirect
final URL so proxied PNG/WebP/GIF attachments are labelled correctly.

Closes #105
forgejo-actions left a comment

WuMing

Found 1 issue(s). See inline comments below.

## WuMing Found **1** issue(s). See inline comments below.
@ -75,3 +75,3 @@
now = datetime.now(UTC)
headers = {"User-Agent": "fenliu-ai/0.1"}
async with httpx2.AsyncClient(timeout=180, headers=headers) as http:
async with httpx2.AsyncClient(timeout=180, headers=headers, follow_redirects=True) as http:

security [HIGH]

Enabling follow_redirects=True on an HTTP client used to fetch post media URLs allows an attacker-controlled media_proxy URL to redirect the server-side request to internal addresses (e.g. cloud metadata 169.254.169.254) or other internal services, a Server-Side Request Forgery (A10). Validate/allow-list redirect targets (scheme, host, port) and block private/link-local IPs before following, or use a hardened outbound proxy.

**security** [HIGH] Enabling follow_redirects=True on an HTTP client used to fetch post media URLs allows an attacker-controlled media_proxy URL to redirect the server-side request to internal addresses (e.g. cloud metadata 169.254.169.254) or other internal services, a Server-Side Request Forgery (A10). Validate/allow-list redirect targets (scheme, host, port) and block private/link-local IPs before following, or use a hardened outbound proxy. <!-- wuming:sha256:0f0232ecc01c7e4e690b0a214fd4739f567357c439e61eaca714b032e509c6ff -->
marvin8 marked this conversation as resolved
Author
Collaborator

packages/fenliu/src/fenliu/services/ai_pipeline.py

Enabling follow_redirects=True on an HTTP client used to fetch post media URLs allows an
attacker-controlled media_proxy URL to redirect the server-side request to internal addresses…

🔴 Declining for this PR, tracked properly in #108. The SSRF primitive predates this change: download_first_image already fetched attacker-chosen preview_url/url values directly, so the initial URL needs the same scheme/DNS/private-range validation as the redirect hops — a redirect-only guard here would be a partial fix with false assurance. #105's scope was to restore intended behaviour (media_proxy posts were stuck unclassified); the full hardening (initial URL plus every redirect hop, IPv4/IPv6 private and link-local ranges) is designed in #108.

[`packages/fenliu/src/fenliu/services/ai_pipeline.py`](https://forge.marvin8.zone/marvin8/dujiangyan/pulls/107#issuecomment-2219) > Enabling follow_redirects=True on an HTTP client used to fetch post media URLs allows an > attacker-controlled media_proxy URL to redirect the server-side request to internal addresses… 🔴 Declining for this PR, tracked properly in #108. The SSRF primitive predates this change: `download_first_image` already fetched attacker-chosen `preview_url`/`url` values directly, so the initial URL needs the same scheme/DNS/private-range validation as the redirect hops — a redirect-only guard here would be a partial fix with false assurance. #105's scope was to restore intended behaviour (media_proxy posts were stuck unclassified); the full hardening (initial URL plus every redirect hop, IPv4/IPv6 private and link-local ranges) is designed in #108.
⬆️ bump deps and pre-commit hooks, fix deprecated asyncio call
All checks were successful
/ publish (pull_request) Has been skipped
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 5m28s
/ gitleaks (pull_request) Successful in 26s
/ checks (pull_request) Successful in 3m0s
f10eac6f77
Newer ty flags asyncio.iscoroutinefunction as deprecated (removed in
Python 3.16), so also swap both circuit_breaker call sites to
inspect.iscoroutinefunction — the documented replacement, equivalent
on Python 3.12+. Pure mechanical swap; behaviour unchanged and
covered by the existing circuit-breaker tests.
forgejo-actions left a comment

WuMing

Found 2 issue(s). See inline comments below.

## WuMing Found **2** issue(s). See inline comments below.
@ -10,3 +10,4 @@
---
Some Mastodon attachments are served through a redirecting `media_proxy` URL rather than a direct file link. The AI vision stage's image downloader treated those redirects as errors, so posts with proxy-served media never received a vision verdict and had to be reviewed by hand. It now follows the redirect to the real file — and reads the image type from the final URL, so proxied PNG/WebP/GIF attachments are labelled correctly instead of always assumed JPEG.

docs [LOW]

Grammar: 'instead of always assumed JPEG' is incomplete. Change to 'instead of always being assumed to be JPEG' or 'instead of always assuming JPEG' to make the meaning clear.

**docs** [LOW] Grammar: 'instead of always assumed JPEG' is incomplete. Change to 'instead of always being assumed to be JPEG' or 'instead of always assuming JPEG' to make the meaning clear. <!-- wuming:sha256:5cefdcb28dc34b6c5b6ce6b295131f2a7b7b8a48a6e7355e8828ecd00b959287 -->
@ -285,3 +285,3 @@
def decorator(func: Callable) -> Callable:
if asyncio.iscoroutinefunction(func):
if inspect.iscoroutinefunction(func):

tests [LOW]

No test file appears in the diff for this zhongli source change. Replacing asyncio.iscoroutinefunction with inspect.iscoroutinefunction (also at line 324) changes how coroutine functions are detected by the circuit-breaker decorators; add or update tests covering sync and async callables so this behavior is verified.

**tests** [LOW] No test file appears in the diff for this zhongli source change. Replacing asyncio.iscoroutinefunction with inspect.iscoroutinefunction (also at line 324) changes how coroutine functions are detected by the circuit-breaker decorators; add or update tests covering sync and async callables so this behavior is verified. <!-- wuming:sha256:2cb8a7e3c2ae313e22cbfbc3aa542ae8ce26a2ac455fbe8a49d6a6631f6bd81d -->
marvin8 marked this conversation as resolved
forgejo-actions left a comment

WuMing

Found 2 issue(s). See inline comments below.

## WuMing Found **2** issue(s). See inline comments below.
@ -10,3 +10,4 @@
---
Some Mastodon attachments are served through a redirecting `media_proxy` URL rather than a direct file link. The AI vision stage's image downloader treated those redirects as errors, so posts with proxy-served media never received a vision verdict and had to be reviewed by hand. It now follows the redirect to the real file — and reads the image type from the final URL, so proxied PNG/WebP/GIF attachments are labelled correctly instead of always assumed JPEG.

docs [LOW]

Clarity: the final phrase 'instead of always assumed JPEG' is elliptical and awkward. Consider 'instead of always being assumed to be JPEG' or 'instead of always falling back to image/jpeg'.

**docs** [LOW] Clarity: the final phrase 'instead of always assumed JPEG' is elliptical and awkward. Consider 'instead of always being assumed to be JPEG' or 'instead of always falling back to image/jpeg'. <!-- wuming:sha256:5cefdcb28dc34b6c5b6ce6b295131f2a7b7b8a48a6e7355e8828ecd00b959287 -->
@ -6,3 +6,3 @@
import asyncio
import functools
import inspect

tests [LOW]

circuit_breaker.py is modified to switch from asyncio.iscoroutinefunction to inspect.iscoroutinefunction in two places, but no zhongli test file appears in this diff. Add or update tests that exercise the async/sync wrapper detection paths, or explicitly document this as an equivalent no-behavior-change dependency/API swap.

**tests** [LOW] circuit_breaker.py is modified to switch from asyncio.iscoroutinefunction to inspect.iscoroutinefunction in two places, but no zhongli test file appears in this diff. Add or update tests that exercise the async/sync wrapper detection paths, or explicitly document this as an equivalent no-behavior-change dependency/API swap. <!-- wuming:sha256:9cc0de6f46f4a9979b3343cc3d17b89a8de83d9e859fe502132e49917bc031a6 -->
marvin8 marked this conversation as resolved
🔧 re-run CI: transient runner failures (pysentry segfault, known flaky test)
All checks were successful
/ publish (pull_request) Has been skipped
/ publish-container (pull_request) Has been skipped
/ checks (pull_request) Successful in 3m47s
/ gitleaks (pull_request) Successful in 13s
/ deploy-docs (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 4m27s
49634a172b
forgejo-actions left a comment

WuMing

Found 1 issue(s). See inline comments below.

## WuMing Found **1** issue(s). See inline comments below.
@ -138,0 +166,4 @@
with (
patch("fenliu.services.ai_pipeline.text_classifier", return_value=text),
patch("fenliu.services.ai_pipeline.vision_classifier", return_value=vision),
patch("fenliu.services.ai_pipeline.httpx2", new=SimpleNamespace(AsyncClient=TransportInjectedClient)),

code [LOW]

Patching the entire fenliu.services.ai_pipeline.httpx2 module with SimpleNamespace only provides AsyncClient. This will hide or break any other httpx2 attributes used by ai_pipeline (such as HTTPError or Response), and it makes the test dependent on the current implementation. Prefer patching only httpx2.AsyncClient or using a small namespace that preserves all existing module attributes.

**code** [LOW] Patching the entire fenliu.services.ai_pipeline.httpx2 module with SimpleNamespace only provides AsyncClient. This will hide or break any other httpx2 attributes used by ai_pipeline (such as HTTPError or Response), and it makes the test dependent on the current implementation. Prefer patching only httpx2.AsyncClient or using a small namespace that preserves all existing module attributes. <!-- wuming:sha256:184580d26c8665bfc40fe5fce897fe9f65f8b6c2320a99c2e2afebb33bc7e2fd -->
forgejo-actions left a comment

WuMing

Found 1 issue(s). See inline comments below.

## WuMing Found **1** issue(s). See inline comments below.
@ -5,3 +5,3 @@
fenliu 3.0.0
-
- Fixed: AI vision now follows media-proxy redirects, so proxy-served attachments get classified instead of stalling as pending.

docs [LOW]

Terminology is inconsistent with line 13: line 13 uses Mastodon's media_proxy identifier, while this line uses 'media-proxy'. Use media_proxy for the code/feature name, or use 'media proxy' as a common noun.

**docs** [LOW] Terminology is inconsistent with line 13: line 13 uses Mastodon's `media_proxy` identifier, while this line uses 'media-proxy'. Use `media_proxy` for the code/feature name, or use 'media proxy' as a common noun. <!-- wuming:sha256:218f589771b919fedba2515d8decb946f49b30837ab93301fce000a93d522889 -->
📝 (fenliu) polish release-note wording and tighten redirect-test patch
All checks were successful
/ publish (pull_request) Has been skipped
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 4m32s
/ gitleaks (pull_request) Successful in 13s
/ checks (pull_request) Successful in 3m44s
5af9684b72
- Reword the ellipsis 'instead of always assumed JPEG' and unify on
  Mastodon's media_proxy identifier.
- Patch only httpx2.AsyncClient in the redirect test instead of
  replacing the whole module namespace.
Author
Collaborator

WuMing waves 201–204 — responses

Release-Notes.md

Grammar: 'instead of always assumed JPEG' is incomplete…

Fixed in commit 5af9684: now reads 'instead of always falling back to image/jpeg'.


zhongli/circuit_breaker.py

No test file appears in the diff for this zhongli source change…

Covered by existing tests, no new test needed: tests/test_circuit_breaker.py already exercises both wrapper-selection paths — sync (circuit_breaker_decorator_simple, async circuit_protected_call success/failure/circuit-open, circuit_breaker_decorator async success). All pass with the swapped predicate; the swap is behaviour-equivalent on Python 3.12+, as documented in the commit message. The duplicate finding in wave 202 (9cc0de6f…) is answered by this reply.


fenliu/tests/test_ai_pipeline.py

Patching the entire fenliu.services.ai_pipeline.httpx2 module with SimpleNamespace only provides AsyncClient…

Fixed in commit 5af9684: the test now patches only httpx2.AsyncClient directly; the rest of the module stays intact.


Release-Notes.md

Terminology is inconsistent with line 13: line 13 uses Mastodon's media_proxy identifier, while this line uses 'media-proxy'…

Fixed in commit 5af9684: the bullet now uses media_proxy.

### WuMing waves 201–204 — responses [`Release-Notes.md`](https://forge.marvin8.zone/marvin8/dujiangyan/pulls/107#issuecomment-2256) > Grammar: 'instead of always assumed JPEG' is incomplete… ✅ Fixed in commit `5af9684`: now reads 'instead of always falling back to image/jpeg'. --- [`zhongli/circuit_breaker.py`](https://forge.marvin8.zone/marvin8/dujiangyan/pulls/107#issuecomment-2257) > No test file appears in the diff for this zhongli source change… ✅ Covered by existing tests, no new test needed: `tests/test_circuit_breaker.py` already exercises both wrapper-selection paths — sync (`circuit_breaker_decorator_simple`, async `circuit_protected_call` success/failure/circuit-open, `circuit_breaker_decorator` async success). All pass with the swapped predicate; the swap is behaviour-equivalent on Python 3.12+, as documented in the commit message. The duplicate finding in wave 202 (`9cc0de6f…`) is answered by this reply. --- [`fenliu/tests/test_ai_pipeline.py`](https://forge.marvin8.zone/marvin8/dujiangyan/pulls/107#issuecomment-2263) > Patching the entire fenliu.services.ai_pipeline.httpx2 module with SimpleNamespace only provides AsyncClient… ✅ Fixed in commit `5af9684`: the test now patches only `httpx2.AsyncClient` directly; the rest of the module stays intact. --- [`Release-Notes.md`](https://forge.marvin8.zone/marvin8/dujiangyan/pulls/107#issuecomment-2265) > Terminology is inconsistent with line 13: line 13 uses Mastodon's `media_proxy` identifier, while this line uses 'media-proxy'… ✅ Fixed in commit `5af9684`: the bullet now uses `media_proxy`.
forgejo-actions left a comment

WuMing

Found 2 issue(s). See inline comments below.

## WuMing Found **2** issue(s). See inline comments below.
@ -10,3 +10,4 @@
---
Some Mastodon attachments are served through a redirecting `media_proxy` URL rather than a direct file link. The AI vision stage's image downloader treated those redirects as errors, so posts with proxy-served media never received a vision verdict and had to be reviewed by hand. It now follows the redirect to the real file — and reads the image type from the final URL, so proxied PNG/WebP/GIF attachments are labelled correctly instead of always falling back to image/jpeg.

docs [LOW]

The MIME literal image/jpeg should be wrapped in backticks for consistency with the inline-code style used for media_proxy earlier in the same release note.

**docs** [LOW] The MIME literal `image/jpeg` should be wrapped in backticks for consistency with the inline-code style used for `media_proxy` earlier in the same release note. <!-- wuming:sha256:7be4f6c534c8f29ef1983fba865d9075e15cff051c61453ddbe8c41d598e8f8c -->

docs [LOW]

The em dash in "real file — and reads" separates two parallel actions awkwardly. Consider wording it as "real file and reads the image type" or splitting into two sentences for clearer prose.

**docs** [LOW] The em dash in "real file — and reads" separates two parallel actions awkwardly. Consider wording it as "real file and reads the image type" or splitting into two sentences for clearer prose. <!-- wuming:sha256:7be4f6c534c8f29ef1983fba865d9075e15cff051c61453ddbe8c41d598e8f8c -->
📝 (fenliu) backtick image/jpeg and smooth release-note sentence
All checks were successful
/ publish (pull_request) Has been skipped
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 4m48s
/ gitleaks (pull_request) Successful in 21s
/ checks (pull_request) Successful in 3m1s
f25f2b34ab
Author
Collaborator

Release-Notes.md

The MIME literal image/jpeg should be wrapped in backticks… / The em dash in 'real file — and reads' separates two parallel actions awkwardly…

Both fixed in commit f25f2b3: image/jpeg is now inline code and the sentence reads 'follows the redirect to the real file and reads the image type from the final URL'.

[`Release-Notes.md`](https://forge.marvin8.zone/marvin8/dujiangyan/pulls/107#issuecomment-2269) > The MIME literal `image/jpeg` should be wrapped in backticks… / The em dash in 'real file — and reads' separates two parallel actions awkwardly… ✅ Both fixed in commit `f25f2b3`: `image/jpeg` is now inline code and the sentence reads 'follows the redirect to the real file and reads the image type from the final URL'.
forgejo-actions left a comment

WuMing

Found 1 issue(s). See inline comments below.

## WuMing Found **1** issue(s). See inline comments below.
@ -10,3 +10,4 @@
---
Some Mastodon attachments are served through a redirecting `media_proxy` URL rather than a direct file link. The AI vision stage's image downloader treated those redirects as errors, so posts with proxy-served media never received a vision verdict and had to be reviewed by hand. It now follows the redirect to the real file and reads the image type from the final URL, so proxied PNG/WebP/GIF attachments are labelled correctly instead of always falling back to `image/jpeg`.

docs [LOW]

The wording "reads the image type from the final URL" overstates the implementation; the fix guesses/derives the MIME type from the final URL's file extension and falls back to image/jpeg when no extension is recognized. For accuracy, consider wording like "infers the image type from the final URL's file extension".

**docs** [LOW] The wording "reads the image type from the final URL" overstates the implementation; the fix guesses/derives the MIME type from the final URL's file extension and falls back to image/jpeg when no extension is recognized. For accuracy, consider wording like "infers the image type from the final URL's file extension". <!-- wuming:sha256:d001851d68b832dea290e0ea29c6b64be325020a234a79fd0716278b12bc9c4b -->
📝 (fenliu) say the image type is inferred from the file extension
All checks were successful
/ publish (pull_request) Has been skipped
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 4m30s
/ gitleaks (pull_request) Successful in 23s
/ checks (pull_request) Successful in 2m59s
30fa68793f
Author
Collaborator

Release-Notes.md

The wording 'reads the image type from the final URL' overstates the implementation…

Fixed in commit 30fa687: the note now says it 'infers the image type from the final URL's file extension'.

[`Release-Notes.md`](https://forge.marvin8.zone/marvin8/dujiangyan/pulls/107#issuecomment-2274) > The wording 'reads the image type from the final URL' overstates the implementation… ✅ Fixed in commit `30fa687`: the note now says it 'infers the image type from the final URL's file extension'.
forgejo-actions left a comment

WuMing

Found 1 issue(s). See inline comments below.

## WuMing Found **1** issue(s). See inline comments below.
@ -5,3 +5,3 @@
fenliu 3.0.0
-
- Fixed: AI vision now follows `media_proxy` redirects, so proxy-served attachments get classified instead of stalling as pending.

docs [LOW]

Minor clarity/subject mismatch: “proxy-served attachments” cannot “stall as pending”; it is the post or its review status that remains pending. Consider rephrasing to something like “so posts with proxy-served attachments get classified instead of remaining pending.”

**docs** [LOW] Minor clarity/subject mismatch: “proxy-served attachments” cannot “stall as pending”; it is the post or its review status that remains pending. Consider rephrasing to something like “so posts with proxy-served attachments get classified instead of remaining pending.” <!-- wuming:sha256:77aae0eeaf2e9d8c2c2c0dd30c8e44a358fb282a5ad4279c1c0d03df53276cd9 -->
🐛 (fenliu) stop alembic fileConfig disabling existing loggers
All checks were successful
/ publish (pull_request) Has been skipped
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 4m31s
/ gitleaks (pull_request) Successful in 22s
/ checks (pull_request) Successful in 3m44s
ea42a15831
Root cause of the intermittent empty log-capture failures (#110):
alembic's env.py called fileConfig with its default
disable_existing_loggers=True, which sets disabled=True on every
logger existing at that moment — including fenliu.__main__. Tests
sharing a worker with migration-running tests (test_database) then
captured nothing, flaking test_main and test_startup order-dependently;
in production the 'Database migrations applied' line was silently
dropped for the same reason. Pass disable_existing_loggers=False, the
Alembic-documented setting for apps that own their logging.

Also make both log-capture helpers restore logger.disabled (belt and
braces) and reword a release-note bullet per WuMing.

Closes #110
Author
Collaborator

Release-Notes.md

Minor clarity/subject mismatch: 'proxy-served attachments' cannot 'stall as pending'…

Fixed in commit ea42a15: the bullet now reads 'so posts with proxy-served attachments get classified instead of remaining pending'.

[`Release-Notes.md`](https://forge.marvin8.zone/marvin8/dujiangyan/pulls/107#issuecomment-2278) > Minor clarity/subject mismatch: 'proxy-served attachments' cannot 'stall as pending'… ✅ Fixed in commit `ea42a15`: the bullet now reads 'so posts with proxy-served attachments get classified instead of remaining pending'.
marvin8 approved these changes 2026-09-08 04:35:51 +00:00
marvin8 manually merged commit 4f9651138a into main 2026-09-08 04:37:07 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
marvin8/dujiangyan!107
No description provided.