How to Fix ERROR_UPLOAD When Sending an Image CAPTCHA

ERROR_UPLOAD means no usable image data reached the solver. The request itself was fine and the API key was fine, but the part that was supposed to carry the picture arrived empty, or never arrived at all. It is one of four codes covering everything that can go wrong on the way in, and they are easy to confuse because all four look identical from your side. This post separates them, gives the real limits, and shows upload code that does not trip any of them.
What you need
- CapSkip running, either in Local mode on the loopback address or in Server mode on a machine your code can reach. Both are described under connection settings, and the choice changes nothing about the errors below.
- Python 3.10 or newer with the SDK installed, or curl if you would rather hit the endpoint directly.
- One real CAPTCHA image saved to disk. A screenshot of a whole page is the wrong test case and sends you chasing the size limit instead of the bug.
What each image error code means
These are the codes the API returns for image submissions, straight from the CapSkip API documentation. Read the meaning column first, because what separates them is where the failure happened, not how bad it was.
| Code | What it means | Where the problem is |
|---|---|---|
| ERROR_UPLOAD | No image data provided, or the upload failed | The request body. Nothing usable arrived |
| ERROR_INVALID_IMAGE | Invalid image format, or corrupted image data | The bytes arrived but do not decode as an image |
| ERROR_INVALID_BASE64 | Invalid base64 encoding | The string never made it back to bytes |
| ERROR_TOO_BIG_CAPTCHA_FILESIZE | Over 600 kB, or over 1000px in a dimension | The image is real and valid, just too large |
| ERROR_BAD_PARAMETERS | Missing or invalid required parameters | The form fields around the image |
There is a fifth failure that never reaches the API at all. Hand the SDK a path that does not exist and it raises a ValidationException naming the file, without making a request. That distinction is worth internalising early: a Python exception means your own machine stopped it, and a code beginning with ERROR means the solver stopped it.
Step 1: send the image in a shape the API accepts
The SDK takes three input forms and turns all of them into the same submission. A local path, a remote URL, or a data URI carrying the image inline.
# pip install capskip
from capskip import CapSkip
solver = CapSkip(host="127.0.0.1", port=8080)
# All three of these are accepted by the same method.
result = solver.normal("captcha.png")
result = solver.normal("https://example.com/captcha.jpg")
result = solver.normal("data:image/png;base64,iVBORw0KGgo...")
print(result["code"]) # the recognized textUse an absolute path rather than a relative one. A relative path resolves against the working directory of the process, which is rarely the directory you wrote the code in, and this is the most common reason a script that works in your editor fails under a scheduler or a service.
On the raw HTTP API there are two ways to carry the image, and picking the wrong field is what produces ERROR_UPLOAD most often. Multipart sends the file itself, and base64 sends it as a string.
# no install needed, curl only # method=post carries the file as multipart/form-data curl -X POST "http://127.0.0.1:8080/in.php" \ -F "key=capskip" \ -F "method=post" \ -F "[email protected]" # method=base64 carries the same image as a string curl -X POST "http://127.0.0.1:8080/in.php" \ -d "key=capskip" \ -d "method=base64" \ --data-urlencode "body=BASE64_STRING_HERE"
Note the field names. With the multipart method the image goes in a field called file, and with the base64 method it goes in a field called body. Sending a file in the body field, or dropping the @ that tells curl to read the file rather than send its name as literal text, both produce an upload with no image in it.
Step 2: fix ERROR_INVALID_BASE64
Base64 failures are almost always line wrapping. GNU coreutils wraps its output at 76 characters by default, and those newlines survive into the request, so pass a wrap width of zero when you encode at a shell prompt. Encoders disagree about this and some do not wrap at all, which is the argument for encoding in code rather than at a prompt.
One more distinction catches people moving between the SDK and the raw API. The SDK accepts a full data URI, prefix included. The body parameter on the raw endpoint wants the base64 payload on its own, with no prefix in front of it.
# pip install capskip
import base64
with open("captcha.png", "rb") as fh:
raw = base64.b64encode(fh.read()).decode()
# One continuous string. No newlines, no spaces, no truncation.
assert "\n" not in raw and len(raw) > 50
# The SDK wants the prefix, the raw body parameter does not.
result = solver.normal("data:image/png;base64," + raw)That length check matters more than it looks. A base64 string shorter than about fifty characters is read as a file path rather than as image data, so a truncated payload does not fail as bad base64 at all. It fails as a missing file, which sends you looking in the wrong place.
Step 3: fix ERROR_TOO_BIG_CAPTCHA_FILESIZE
This one has published numbers rather than a vague guideline. The API rejects an image over 600 kB, and it rejects one over 1000px in either dimension. Both limits are generous for an actual CAPTCHA, so hitting either usually means a full page screenshot went out instead of the widget.
Cropping to the CAPTCHA is the better fix, because it improves accuracy at the same time. Downscaling is the fallback for when you cannot crop reliably.
# pip install capskip pillow
from PIL import Image
img = Image.open("captcha.png")
# Bring the longest side under the documented 1000px ceiling.
if max(img.size) > 1000:
img.thumbnail((1000, 1000))
# optimize=True usually clears the 600 kB limit on its own.
img.save("captcha-ready.png", optimize=True)Step 4: fix ERROR_INVALID_IMAGE
This code means the bytes arrived intact and are not an image. In practice it has one dominant cause. You downloaded the CAPTCHA with an HTTP client, the server answered with an error page or a redirect, and your code saved the response body to a file ending in .png without ever looking inside it. The file is real, the size is plausible, and the first four bytes are HTML.
# no install needed, standard library only
SIGNATURES = (b"\x89PNG", b"\xff\xd8\xff", b"GIF8", b"RIFF")
with open("captcha.png", "rb") as fh:
head = fh.read(4)
# An error page saved as .png starts with <!DO or <htm instead.
print(any(head.startswith(sig) for sig in SIGNATURES))Run that check at download time rather than at solve time. A four byte read is free, and it turns a confusing solver error into an obvious network error at the point where the network error actually happened.
Full working example
Putting the four fixes together gives a function that validates before it sends, and keeps the errors your machine raised separate from the errors the solver returned.
# pip install capskip pillow
from PIL import Image
from capskip import CapSkip, ApiException, ValidationException
solver = CapSkip(host="127.0.0.1", port=8080, defaultTimeout=120)
def solve_image(path):
"""Return the CAPTCHA text, or None with a reason printed."""
img = Image.open(path) # raises if it is not an image
if max(img.size) > 1000:
img.thumbnail((1000, 1000))
img.save(path, optimize=True)
try:
return solver.normal(path)["code"]
except ValidationException as exc:
print("never left this machine:", exc)
except ApiException as exc:
print("the solver rejected it:", exc)
return NoneOpening the image with Pillow first collapses two of the four codes into one local exception, because a file that cannot be opened here was never going to decode there either. What reaches the API afterwards is a known good image at a known good size.
One thing not to add: a proxy. Proxies are supported for reCAPTCHA, Turnstile, GeeTest and CaptchaFox, and not for image CAPTCHAs. Passing one is a parameter error rather than a silent no-op, so it fails the call outright.
Running the solver on a server instead
Image uploads are the one CAPTCHA type where the request carries real payload, so it is worth knowing where that payload is going. CapSkip offers two arrangements:
| Mode | Listens on | Use it when |
|---|---|---|
| Local | 127.0.0.1, that device only | Your script and the solver run on one machine |
| Server | Your network address or public IP | Workers, a VPS or a hosted platform call in over the API |
In Server mode you point the SDK host at the solver machine and nothing else in your code changes, so a pool of workers can share one instance. A static public IP is recommended when the callers sit outside your own network. The details are under connection settings, and Server mode is still your hardware and still unmetered: it moves where the solver runs, never who owns it or how it bills.
FAQ
What is the maximum image size CapSkip accepts?
600 kB, and 1000px in either dimension. Cross either one and you get ERROR_TOO_BIG_CAPTCHA_FILESIZE rather than a partial solve. Both numbers are published on the API reference page, and both sit comfortably above any genuine CAPTCHA image, so treat hitting them as a sign you are sending more of the page than you meant to.
Why does the same image work locally but fail on my server?
Usually the path, not the image. A relative path resolves against whatever working directory the process was started in, and a service or a scheduled task rarely starts where you think it does. Use an absolute path, then check the file is readable by the account the process runs as. If the solver itself is on another machine, confirm the host and port point at it rather than at loopback.
Can I use a proxy when solving image CAPTCHAs?
No. Proxy support covers reCAPTCHA, Turnstile, GeeTest and CaptchaFox, which are the types where the solver loads the page itself. An image CAPTCHA is bytes you already hold, so there is nothing for a proxy to do and the parameter is rejected rather than ignored.
How long should an image CAPTCHA take?
Well under a second in normal operation. The polling timeout for image work defaults to 120 seconds, which is a safety net rather than an expectation, and the SDK starts polling at a quarter of a second and backs off from there. If you are waiting seconds rather than milliseconds, the interesting question is what the solver is doing, not whether your timeout is long enough.
The shortest version
Work out which side failed before changing anything. A Python exception means the image never left your machine, and a code beginning with ERROR means it arrived and was rejected. Then match the code: nothing arrived is ERROR_UPLOAD, arrived but will not decode is ERROR_INVALID_IMAGE, the string will not decode is ERROR_INVALID_BASE64, and too large means past 600 kB or past 1000px. Validate the bytes at download time and most of these stop happening. A failed solve after a clean upload is a different problem, covered in the guide to ERROR_CAPTCHA_UNSOLVABLE. For the parameters themselves, the image CAPTCHA solver page is the reference, and the Python specifics live on the Python CAPTCHA solver page. The economics help here too: unlimited captcha solver capacity on hardware you own means a retry after a bad upload costs nothing but the retry itself.
