Troubleshooting
Symptom first. Each entry lists the failure you see and its likely causes, most likely first. For what each error class carries, see Errors.
401 on every request
The API key is missing or malformed.
- Pass the key as
apiKeywhen you create the client. The SDK sendsAuthorization: Bearer <apiKey>itself; do not prefix the key withBearer, or the header becomesBearer Bearer …and every call fails. - If you set custom headers yourself, the SDK does not add
Authorizationfor you unlessapiKeyis also set. - Check the key has not been revoked.
413 when uploading a large file
Files above the multipart threshold (10 MB by default) upload in parts, and the first part is always exactly 8 MB. The SDK floors the threshold at 8 MB for that reason, so a threshold you set lower is raised to 8 MB rather than producing a malformed upload.
A 413 on a file below the threshold is not a multipart problem. Read the
error code: image_dimensions_too_large means the image's pixel area is over
the cap for raster images, whatever its size in bytes; otherwise it is your
plan's per-upload limit, so check your account limits.
GislValidationError when creating a workflow
The payload does not match the operation's schema. details[] names each field
and what is wrong with it. getSchema() returns the options every operation
accepts for each media type. The usual causes:
- An option that does not exist for that operation or media type, or one that
exists only on some routes. Image compress has no
modeoricc_profile: metadata is controlled bymetadata(strip, the default, orkeep, which preserves EXIF, colour profile and XMP on a same-format compress). - A
converttarget format the input media type cannot be converted to. mergeandarchivebuilt by hand with a singlesource: they take a job withinputs[]. The file-firstmerge(...)andfiles(...).archive(...)build that for you.
The SSE stream ends without a terminal event
A proxy timeout, a sleeping laptop, or a dropped connection closed the stream. The server closes it silently, so iteration simply ends rather than throwing.
- If you need the final state for certain, fetch it once the stream closes with
getWorkflowStatus(workflowId). run()handles this for you: it streams over SSE and falls back to polling if the stream drops. See Progress streaming (SSE).
SSE events arrive in bursts
Something between you and GISL is buffering the response: a corporate HTTP proxy
or an edge rewrite rule. GISL sends Cache-Control: no-transform, but some edge
configurations strip it. Confirm by opening the stream from outside the proxy.
There is no client-side fix.
Webhook verification fails
The body you verify is not the bytes GISL signed. Your framework almost certainly parsed the JSON first. Verify the raw body, then parse it. If the raw body is right, the secret is wrong: it belongs to another workflow, or the workflow was re-created. See Webhooks.
Invalid signature format means the sha256= prefix is missing from the header
value you passed. Pass the header value unchanged.
A request times out during a large upload
- TypeScript
- PHP
- Python
- Rust
timeout (default 30 seconds) applies to each request to the GISL API, such as
multipart initiate and complete. It does not cover the part uploads to storage.
Raise it for very large or metadata-heavy files rather than globally.
To cap the whole upload, pass an AbortSignal driven by your own timer.
Aborting cancels the API requests and every part upload, and the upload rejects
with GislAbortError:
import { GislAbortError } from '@giveitsmaller/sdk';
const controller = new AbortController();
const cap = setTimeout(() => controller.abort(), 5 * 60_000); // 5-minute ceiling
try {
await client.uploadFile('./huge-video.mp4', { signal: controller.signal });
} catch (err) {
if (err instanceof GislAbortError) {
// clean up, or surface it to the caller
}
throw err;
} finally {
clearTimeout(cap);
}
Part uploads retry transient failures (5xx, 429, network errors) up to
multipartMaxAttempts attempts in total (default 3) with jittered exponential
backoff from multipartRetryBaseMs (default 500 ms). Other 4xx responses and
aborts fail at once. When the attempts run out, the upload fails with
GislMultipartPartError, which carries partNumber and uploadId. Set
multipartMaxAttempts: 1 to turn retries off.
PSR-18 has no standard timeout setting, so the SDK records timeoutMs but does
not enforce it. Timeouts are enforced by the HTTP client you inject: set
timeout and connect_timeout on Guzzle, or timeout on Symfony HttpClient.
Not available: the Python SDK is not published and no release is scheduled — see Python.
Not an application SDK: Rust is internal contract types — see Rust.
GislTimeoutError while waiting for a workflow
The workflow did not reach a terminal status within the wait deadline. The
low-level waitForWorkflow() waits 10 minutes by default in both SDKs; pass
a longer timeoutMs for heavy inputs such as long videos or large archives.
run() takes its own maxWait.
A timeout ends your wait, not the workflow. Do not re-run it: the work may
still complete. Check it with getWorkflowStatus(), or recover the result as
described in Recovering a timed-out run().
CORS errors in the browser
The API's CORS allows browser requests only from GISL's own web app, so calling
the API directly from your site is blocked. Run the SDK on your backend and have
your frontend call your own API. Downloading an output URL with fetch() from
your origin is also blocked by the storage bucket's CORS; link to it or download
it server-side. See TypeScript SDK.
TypeScript: type errors on OperationDef.options
In the low-level API options is Record<string, unknown>. Check an options
object against its typed interface without a cast using satisfies:
import { OperationType, type CompressImageOptions } from '@giveitsmaller/sdk';
const op = {
type: OperationType.compress,
options: { quality: 80 } satisfies CompressImageOptions,
};
The ergonomic builders' option bags are typed already: client.compress(input, options) and
client.file(...).compress(optimize, options) take CompressOptions, and a misspelled key is a
compile error.
No webhookSecret on the created workflow
No callback URL was set. The secret is only generated when the workflow has one. Re-create the workflow with a callback URL if you need it.
Downloads come back empty
The workflow has not reached a terminal status, or its jobs failed and produced
nothing. Check getWorkflowStatus() first. A failed workflow has no outputs;
in a partially_failed one, the jobs that succeeded have downloads and the
failed ones do not.
Still stuck?
Contact support with: the SDK and runtime versions, the workflow id, the request path, the full error message and stack trace, and a minimal payload that reproduces it.