Repository navigation
web_fetch: one unpaired surrogate from HTML truncation permanently breaks a session (HTTP 400) #10713
userszhangkefan
started this conversation in
General
Replies: 1 comment
|
补充一条链接:这条与 #11476、#11831 是同一个根因(一次 UTF-16 码元切把代理对劈开)。 #11831 上整理了:
如果 web_fetch 的 HTML 截断有自己的切片点,也值得一并检查是否走码元切。 |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Summary
A single
web_fetchof an ordinary third-party page can permanently break a session. The tool result ends up containing an unpaired UTF-16 surrogate;JSON.stringifyemits it as\ud83cin every later request body, the gateway rejects the request with a bare400 INVALID_REQUEST(noerror.message, so the UI can only showDeepSeek Messages request failed (400)), and because the character stays in session history every subsequent request fails the same way. The session is unrecoverable; only a new session works.Environment
deepseek-official/deepseek-flashweb-fetch-httpdefaults:maxBodyChars = 100_000Reproduction
A purpose-built, deterministic page (no dependency on a third-party site whose HTML may change): http://weswms.art/slice_errors/
It is 100 053 UTF-16 code units long, and code unit 99 999 is the high surrogate of an emoji, so the default 100 000 cap always lands between the two halves of that character.
web_fetch http://weswms.art/slice_errors/→ HTTP 200, 4758 characters, rendered markdown ends with[\ud83c](...).{"message":"DeepSeek Messages request failed (400)","code":"INVALID_REQUEST","status":400}, and the same on every later message.Isolated: a fresh session running only
pwsh→web_fetch(that URL) →pwshfails on the third step.Control: the same three steps against a page without an astral character at the boundary pass with no errors.
Root cause
packages/web/web-fetch-http/src/provider.ts:168bounds the decoded body withString.prototype.slicecounts UTF-16 code units. When the cap lands between the two halves of an astral character, the body ends on an unpaired high surrogate.packages/web/tool-web/src/fetch.tshas the same pattern twice more: the source cut at line 243 and the output cut at lines 335–336.Proposed fix
Bound every cut on a code point boundary, and sanitize the model-facing text as a backstop. Diff against
master(base blobs a6ddbe0 / f4a644d / 1a134f2 / ee1b478):boundTextis the same helper under tool-web's local name; two regression tests come with it (fetch-http.spec.ts: a body capped mid-pair must not end on an unpaired surrogate;tool-web.spec.ts:formatFetchOutputmust never emit one).Full branch: https://github.com/userszhangkefan/deepseek-harness/tree/fix/web-fetch-lone-surrogate
Why the session dies (and what recovery takes)
The bad code unit is stored in the session log, not just generated on the fly. Reopening the session re-serializes it into every request, so the only recovery is rewriting the stored transcript (replace the escape with
\uFFFD). Worth keeping in mind for users who hit this: new sessions work, the poisoned one never recovers on its own.Two smaller issues this surfaced
providerError()falls back toDeepSeek Messages request failed (<status>)whenever the response body has noerror.message, which makes a malformed request body indistinguishable from a model-side rejection. Keeping the response body (orx-deepseek-request-id) on the failure event would have pointed at the real cause immediately.toWellFormed()before a tool result enters history would cap the blast radius of every provider, including ones that cannot be patched.Interim mitigation I am running locally
Until a release carries the fix, a one-line change to the packaged
@deepseek-ai/dsh-tool-webkeeps the session alive:function formatFetchOutput(result, maxOutputChars) { - return renderFetchOutput(result, maxOutputChars).text; + return renderFetchOutput(result, maxOutputChars).text.toWellFormed(); }With it, fetching the reproduction page yields a
U+FFFDin the markdown instead of a lone surrogate, and the next request succeeds (verified end to end on 0.2.0-rc.2). It is a personal workaround for this install, not a substitute for the source fix.Handover
CONTRIBUTING says external pull requests are not accepted right now, so this is posted here. The branch above is ready to open as a PR the moment that changes, or the diff can be applied directly. Happy to adjust the shape of the fix to whatever the codebase prefers.
All reactions