Skip to content

fix(translate): answer blank texts with themselves instead of failing the request - #245

Merged
missuo merged 4 commits into
OwO-Network:mainfrom
JounQin:feat/blank-text-passthrough
Oct 10, 2026
Merged

missuo merged 4 commits into
OwO-Network:mainfrom
JounQin:feat/blank-text-passthrough

Conversation

@JounQin

@JounQin JounQin commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #244. Nothing about emptiness is an error now: every text is answered — translated when it carries something, returned as it arrived when it is blank — and an empty list is answered with an empty list.

A request used to fail as a whole when any one of its texts was empty, so a document split into lines could not be translated at all if it had a blank line in it. The rule was inconsistent too — "" failed the whole request while whitespace-only passed through, because whitespace survived only as a side effect of oneshot echoing it back.

request before after
["", "Hello"] 404 No text to translate 200 ["", "你好"]
["Hello", ""] 404 200 ["你好", ""]
["", " "] 404 200 ["", " "]
" ", [" "] 200, whitespace sent upstream 200, answered locally
"", [""], ["", ""] 404 200, texts echoed back
"text": [] 404 No text to translate 200 {"data": []} — the request is mirrored
null text 404, then 200 "" on this branch 400 Invalid request payload — a null text names no text
no text field 404 No text to translate 400 Invalid request payload

A null text is the same case as a missing one, and it used to be the opposite one. json.Unmarshal into a string is a documented no-op for null, so {"text":null} decoded to [""] and was indistinguishable from {"text":""} for the rest of the request: 404 on main, and 200 "" on this branch, which nobody asked for. UnmarshalJSON now leaves the zero value for null, so Texts == nil means "the request named no text" — the key is missing or null — and the handler tests exactly that instead of re-deriving the decoder with !Batch && len(Texts) == 0.

The refusals left are therefore a body that never named text and a null one: in both cases the request carries no text, which is a payload without its parameter rather than an empty text, and it is the same 400 the endpoint already answers for a body it cannot bind. "text": [] is not that case: it names a list that happens to be empty, and the response renders it perfectly well.

Forwarding a blank cannot work: oneshot echoes an empty input, and the positional all-or-nothing check reads that echo as a failed translation, so the answer becomes 503 Translation failed. Measured live on 83f97d3 with only the empty-element guard removed, ["", "Hello"] and ["Hello", ""] both answer that 503. The bypass therefore happens here.

Change

  • totalTextLength(texts) becomes textsToTranslate(texts) (positions []int, total int): a blank text is skipped and not charged against the cap, and the zero-text request leaves both empty.
  • TranslateByDLX answers every request: the texts that carry something travel upstream, and mergeTranslations splices the answers back with every blank text left exactly as it arrived. When there is nothing to send, the same call returns the texts unchanged — allocated, so an empty request marshals as [] and not null.
  • PayloadText.UnmarshalJSON leaves its zero value for a null text, so Texts == nil covers both ways of naming no text, and the /translate handler refuses that with 400 Invalid request payload before translating. Any other request mirrors the shape it was given.
  • Tests: textsToTranslate positions and length; pure mergeTranslations; TranslateByDLX answering nil, [], "", [""], ["", ""], whitespace and ["", " "] with themselves; decode tests pinning null and the missing key to a nil Texts against "text": []'s allocated empty slice; handler-level tests for the "data":[] / "data":"" shapes and the no-text 400; and the cap cases on the reduced payload.
  • The README documents the rule.

This matches the JavaScript client (@deeplx/core batchPayload and restoreBlankSegments), which already drops text.trim() === '' segments and restores them.

go build ./..., go vet ./... and go test ./... pass. Live on a build of this branch: ["", "Hello"], ["Hello", ""], ["", " "], [" "], [" ", "Hello"], "", [""], ["", ""] and {"text":[]} all answer 200 with data in the shape the caller used; a body with no text, or "text": null, answers 400 Invalid request payload; ["", "😀"×751] answers 413 text exceeds maximum length: 1502 characters and ["😀"×400, "", "😀"×400] answers 413 … 1600 characters, so blanks are not charged against the cap.

Refs #240, #241.

… the request

A request fails as a whole when any one of its texts is empty, so a document
split into lines cannot be translated at all if it has a blank line in it:
["", "Hello"] answers 404 "No text to translate" and nothing is translated.

A blank text — empty, or nothing but whitespace — carries nothing to
translate, so it is no longer sent, and its own text is the answer. That
keeps the response position-aligned, and it is the only thing that can work:
oneshot echoes an empty input, and the positional check would read that echo
as a failed translation and answer 503 instead. A whitespace-only text
survives today only because oneshot happens to echo it as well.

The 404 is now only for a request with no text at all — no texts, or every
text empty. A request that has text but nothing to translate, like ["   "],
is answered with its own texts, so no blank input errors any more and no
input that used to succeed fails now.

This matches the JavaScript client (@deeplx/core batchPayload and
restoreBlankSegments), which already drops text.trim() === '' segments and
puts them back, so the two sides agree on what a blank segment means.

Refs OwO-Network#240, OwO-Network#241, OwO-Network#244.
Copilot AI balanced review requested due to automatic review settings October 10, 2026 15:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…s well

Every text is answered now, blank or not, so the all-empty case needs no
rule of its own: "" and [""] are texts with nothing to translate in them,
and they are answered with themselves like any other blank text. A missing
or null `text` decodes to the one empty string and is answered the same
way.

The 404 is left for a request that names no text at all: `"text": []`, the
one shape there is nothing to answer with. Nothing else errors on
emptiness.

Refs OwO-Network#244.
`"text": []` names a list that happens to be empty, and the response can
render that perfectly well, so rejecting it bought nothing: it was the
original "No text to translate" condition surviving for the one shape the
code had no answer to. It is answered with `{"data": []}` now — the same
rule as every other text, applied to none of them.

A body that never named `text` is a different thing, a payload without its
parameter: it answers 400 Invalid request payload instead of the 404 that
stood in for it, which is also what the endpoint answers for a body it
cannot bind. That is the only refusal left.

Refs OwO-Network#244.
@JounQin
JounQin force-pushed the feat/blank-text-passthrough branch from b86612e to b28dcd4 Compare October 10, 2026 16:31
UnmarshalJSON sent `null` down the string branch, where json.Unmarshal into a
string is a documented no-op, so `{"text":null}` decoded to `[""]` and was
indistinguishable from `{"text":""}` for the rest of the request. On main both
were an error, but this branch answers an empty text with itself, so null
quietly became a 200 instead.

A JSON null is a present key with no value, so it leaves the zero value now —
the same Texts == nil a body with no `text` key has — and the handler refuses
both with 400 Invalid request payload, testing the field directly instead of
re-deriving the decoder with `!Batch && len(Texts) == 0`. Every shape that
names text, including `"text": []`, still decodes to a non-nil Texts and is
answered rather than refused.

Refs OwO-Network#244.
@missuo
missuo merged commit 0f2b50d into OwO-Network:main Oct 10, 2026
3 checks passed
JounQin added a commit to un-ts/deeplx that referenced this pull request Oct 10, 2026
Nothing about emptiness is an error now, matching OwO-Network/DLX#245 rule for
rule: a blank text (empty or whitespace-only) is answered with itself and is
never sent, an empty array is answered with an empty array, and a blank string
with that same string. Blank segments are not charged against the 1500-character
cap either, because they do not travel.

This replaces the empty-array and all-blank throws (and the 404 a blank string
used to resolve to); the one failure left is a response that does not line up
with the texts that were sent. The CLI no longer drops blank `--text` values, so
a document split into lines keeps them (a blank `--file` path is still not a
file), and the READMEs, the changeset and the tests follow.

The core bundle grows to 3.12kB, so its size-limit budget is raised to 3.2kB.
@JounQin
JounQin deleted the feat/blank-text-passthrough branch October 10, 2026 16:58
JounQin added a commit to un-ts/deeplx that referenced this pull request Oct 10, 2026
Verified against the merged implementation (OwO-Network/DLX#245, ee11142) rather
than against its description alone, which closes three gaps:

- A null or missing element *inside* a list is the empty string, as the Go
  decoder leaves it, instead of being refused; any other non-string element is
  still refused, exactly like the decoder that cannot bind it.
- The languages are resolved before the cap, as the sibling does, so an
  unsupported language is a 400 even when the request is also over the limit.
- The 413 message is the sibling's own sentence again — same code and same text
  for one text or for a list — rather than the batch wording and the unit suffix
  invented here; the README and the code comments still name the UTF-16 unit.

A request that names no text (null, undefined, a wrong type, a list holding
something that is not a string) now resolves to 400 Invalid request payload, the
same payload error the sibling answers. normalizeTexts keeps the any of
Array.isArray out of the validation, so a wrong value cannot reach trim or the
request body as a crash.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Blank text should be returned as it is instead of failing the request

3 participants