Real file sizes, pre-signed uploads, compact responses, and four failures handled.
Part three of three. Real file sizes, the pre-signed upload path, compact responses, and four failures with the handling each one needs.
Parts [one]../part-1/ and [two]../part-2/ used a two page contract. That size hides every problem worth writing about. This part uses a fifty five page tender response with compliance tables, which is an ordinary document in the world where people actually need this.
Python 3.10 or newer, SUPERDOCS_API_KEY set, and the big sample:
python3 sample/make_samples.py --big 55
That writes sample/tender-response.html, roughly 183 KB of invented bid
response: 79 numbered sections, compliance tables every fifth section, no real
company anywhere in it.
Uploading HTML that pretends to be a Word file teaches you nothing. Turn the sample into a genuine .docx first, using the export endpoint, which is free:
payload = client.export(html=big_html, fmt="docx", filename="tender-response.docx")
save_export(payload, "sample/tender-response.docx")
Now there is a real Word file on disk to push through the pipeline.
Here is the number that surprised me: fifty five pages of text is a 0.04 MB .docx. Word compresses text extremely well.
So the size question splits in two, and conflating them is how people pick the wrong upload path:
Only one of those is visible in a page count, and it is not the one that breaks your upload.
INLINE_UPLOAD_LIMIT_BYTES = 20 * 1024 * 1024
if size <= INLINE_UPLOAD_LIMIT_BYTES:
client.upload_base64(path.name, path.read_bytes(), session_id=session_id)
else:
# pre-signed flow
Where that 20 MB comes from, since the endpoint documents 50 MB: base64 inflates a file by about a third, and the hosted gateway rejects any request body over roughly 32 MB before the API ever sees it. So a 25 MB file becomes a 33 MB body and dies at the front door. The API's own limit never gets a say.
The failure mode is the reason to guard it rather than discover it. The gateway answers with HTML, not JSON, so your client raises a JSON decode error on a page of markup and tells you nothing about size at all.
if exc.code == 413 or (exc.code >= 400 and raw.lstrip().startswith("<")):
raise PayloadTooLarge(
f"{path} refused before the API saw it ({exc.code}). "
"The body was too large for the gateway. Use the pre-signed upload flow.")
An HTML body on an API error means something in front of the API answered. That is a useful signal in any stack, not just this one.
Three calls, and the file never passes through your process memory or an agent's context window:
ticket = client.request_upload_url(name, DOCX_TYPE, size_bytes) # POST /v1/uploads
client.put_bytes(ticket["upload_url"], data, DOCX_TYPE) # PUT straight to storage
parsed = client.process_upload(ticket["upload_id"], session_id, name)
Real output:
upload tender-response.docx is 0.04 MB, inline limit is 21 MB
upload ticket 31312e09452945d59c1d50859788f396, PUT url valid 300s
upload bytes are in storage, 2.3s, and never went through the API
parsed 412 chunks via the presigned path, parsing is not billable
412 chunks. Every paragraph, heading, table, row and cell got an addressable ID. That is what makes "replace the body of the data protection section" a targeted edit rather than a rewrite of the file.
Two things to hold on to: the PUT URL is valid for 300 seconds, so request it when you are ready to send, not at the top of your script. And parsing is not billable. You pay when the AI edits, not when you load.
By default the response carries the whole updated document. On a 183 KB document that is most of a context window, per turn.
job_id = client.chat_async(session_id=session_id, message=EDIT,
response_mode="compact")
edit done, 1 operation, 4 section diff(s) returned
edit response carried 5,164 chars of diffs instead of the whole document
5 KB instead of 183 KB, for the same edit. On any document past about twenty pages this should be your default, and if you need to read a section you ask for it in plain language rather than pulling the document back.
I asked it to rewrite the "Data protection" section. It reported four edits.
That is correct, not a bug: my generated sample cycles through twenty section titles four times, so there are four sections called Data protection and it edited all of them. But it is the exact shape of an expensive mistake. If your document has repeated headings, addressing sections by title is ambiguous. The chunk IDs exist for precisely this, and the fix is to name the chunk, not the title.
POST /v1/downloads returns a signed URL valid for fifteen minutes, and streams
to disk without holding the file in memory:
ticket = client.download_url(session_id, fmt="pdf", filename="tender.pdf")
client.save_from_url(ticket["download_url"], "out/tender.pdf")
export tender-response.edited.docx 0.04 MB, link valid 900s
export tender-response.edited.pdf 0.40 MB, link valid 900s
export tender-response.edited.txt 0.18 MB, link valid 900s
When a document is too large to render inside one request, there is an email fallback that runs in the background and mails a link:
except SuperDocsError:
client.request_export_by_email(session_id, fmt=fmt, filename=name)
Wire the fallback even if you never hit it. It is four lines, and the alternative is a pipeline that stops dead on the biggest document a user owns, which is always the one that matters.
These are not hypotheticals. Every one of them happened while writing this, and
--demo-failures triggers all of them on purpose:
python3 part3_files_export/large_documents.py --demo-failures
handled: /v1/uploads/5e72fcd5.../process -> 404 Upload 5e72fcd5... not found in
cloud storage. It may have expired (uploads are auto-deleted after 24 hours) or
never been completed.
An excellent error message, and the right handling is to re-request a ticket and
PUT again rather than retrying process, which will keep saying the same thing.
handled: PUT failed: 403. The pre-signed URL lasts five minutes and is bound to
the content type and size you declared. Ask for a fresh one.
<Error><Code>SignatureDoesNotMatch</Code>...
The signature covers what you declared. Send a different content type or a different length and storage refuses it. Note this error comes from Google Cloud Storage as XML, not from SuperDocs as JSON, so a client that assumes JSON on every error will crash while handling the error.
handled before a single byte left the process: too-big.docx is 21.0 MB. Base64
would push the request past the ~32 MB gateway cap. Use upload_via_url instead.
The cheapest failure is the one that never leaves your machine.
Large multi-section edits can run out of their step budget and stop, waiting. This looks exactly like the approval pause from part two and is answered by a completely different endpoint:
if meta.get("awaiting_kind") == "continue_prompt":
client.continue_job(session_id, job_id, keep_going=True) # fresh budget
else:
... # this is a change approval, see part two
Check metadata.awaiting_kind before you decide which call to make. Sending an
approval to a continue prompt does nothing, and your job sits there until your
timeout fires.
And the timeout is the last piece: on a long document a job can sit at 99 percent
for minutes. A quiet job is a working job. Give wait_for_job a generous
max_wait and, when it expires, say the true thing:
raise SuperDocsError(
f"job {job_id} still {status} after {int(max_wait)}s. "
"It keeps running server side; poll it again rather than starting another.")
Starting another is how you pay twice for one edit.
python3 sample/make_samples.py --big 55
python3 part3_files_export/large_documents.py # inline, since it is small
python3 part3_files_export/large_documents.py --presigned # force the large file path
python3 part3_files_export/large_documents.py --demo-failures
One operation for the edit, everything else free. You end with a fifty five page tender response edited in one targeted place and exported to .docx, .pdf and .txt, plus a verification step that reads the exported text back and confirms the new wording is actually in it.
Because that is the through line of all three parts: the API telling you it worked, and the document being right, are two different claims. Check the second one.
All three are in the docs at docs.superdocs.app.
Code for all three: github.com/vaidyarushikesh7/superdocs-python-tutorials
Written by Rushikesh Vaidya as part of the SuperDocs Round 2 task. Every command and every error message above came from a real run against the live API.